Merge "Centralized comment requests"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 02787a3..2b934b8 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1152,6 +1152,16 @@
 +
 The default limit is 1024kB.
 
+[[change.privateByDefault]]change.privateByDefault::
++
+If set to true, every change created will be private by default.
++
+Note that the newly created change will be public if the `is_private` field in
+link:rest-api-changes.html#change-input[ChangeInput] is set to `false` explicitly
+or the `remove-private` link:user-upload.html#private[PushOption] is used in the push.
++
+The default is false.
+
 [[changeCleanup]]
 === Section changeCleanup
 
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 04ed56a..1ac1afb 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -2551,14 +2551,26 @@
 Compiled plugins and extensions can be deployed to a running Gerrit
 server using the link:cmd-plugin-install.html[plugin install] command.
 
-Web UI plugins distributed as  single `.js` file can be deployed
-without the overhead of JAR packaging, for more information refer to
-link:cmd-plugin-install.html[plugin install] command.
+Web UI plugins distributed as a single `.js` file (or `.html' file for
+Polygerrit) can be deployed without the overhead of JAR packaging. For
+more information refer to link:cmd-plugin-install.html[plugin install]
+command.
 
-Plugins can also be copied directly into the server's
-directory at `$site_path/plugins/$name.(jar|js)`.  The name of
-the JAR file, minus the `.jar` or `.js` extension, will be used as the
-plugin name. Unless disabled, servers periodically scan this
+Plugins can also be copied directly into the server's directory at
+`$site_path/plugins/$name.(jar|js|html)`. For Web UI plugins, the name
+of the file, minus the `.js` or `.html` extension, will be used as the
+plugin name. For JAR plugins, the value of the `Gerrit-PluginName`
+manifest attribute will be used, if provided, otherwise the name of
+the file, minus the `.jar` extension, will be used.
+
+For Web UI plugins, the plugin version is derived from the filename.
+If the filename contains one or more hyphens, the version is taken
+from the portion following the last hyphen. For example if the plugin
+filename is `my-plugin-1.0.js` the version will be `1.0`. For JAR
+plugins, the version is taken from the `Version` attribute in the
+manifest.
+
+Unless disabled, servers periodically scan the `$site_path/plugins`
 directory for updated plugins. The time can be adjusted by
 link:config-gerrit.html#plugins.checkFrequency[plugins.checkFrequency].
 
diff --git a/Documentation/rest-api-plugins.txt b/Documentation/rest-api-plugins.txt
index fb489d7..0f687bf 100644
--- a/Documentation/rest-api-plugins.txt
+++ b/Documentation/rest-api-plugins.txt
@@ -110,6 +110,74 @@
   }
 ----
 
+Prefix(p)::
+Limit the results to those plugins that start with the specified
+prefix.
++
+The match is case sensitive. May not be used together with `m` or `r`.
++
+List all plugins that start with `delete`:
++
+.Request
+----
+  GET /plugins/?p=delete HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "delete-project": {
+      "id": "delete-project",
+      "index_url": "plugins/delete-project/",
+      "version": "2.9-SNAPSHOT"
+    }
+  }
+----
++
+E.g. this feature can be used by suggestion client UI's to limit results.
+
+Regex(r)::
+Limit the results to those plugins that match the specified regex.
++
+Boundary matchers '^' and '$' are implicit. For example: the regex 'test.*' will
+match any plugins that start with 'test' and regex '.*test' will match any
+project that end with 'test'.
++
+The match is case sensitive. May not be used together with `m` or `p`.
++
+List all plugins that match regex `some.*plugin`:
++
+.Request
+----
+  GET /plugins/?r=some.*plugin HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "some-plugin": {
+      "id": "some-plugin",
+      "index_url": "plugins/some-plugin/",
+      "version": "2.9-SNAPSHOT"
+    },
+    "some-other-plugin": {
+      "id": "some-other-plugin",
+      "index_url": "plugins/some-other-plugin/",
+      "version": "2.9-SNAPSHOT"
+    }
+  }
+
+----
 
 Skip(S)::
 Skip the given number of plugins from the beginning of the list.
@@ -138,6 +206,33 @@
   }
 ----
 
+Substring(m)::
+Limit the results to those plugins that match the specified substring.
++
+The match is case insensitive. May not be used together with `r` or `p`.
++
+List all plugins that match substring `project`:
++
+.Request
+----
+  GET /plugins/?m=project HTTP/1.0
+----
++
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "delete-project": {
+      "id": "delete-project",
+      "index_url": "plugins/delete-project/",
+      "version": "2.9-SNAPSHOT"
+    }
+  }
+----
 
 [[install-plugin]]
 === Install Plugin
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 1e7f069..3180eb4 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -1182,7 +1182,7 @@
   ]
 ----
 
-Skip(s)::
+Skip(S)::
 Skip the given number of branches from the beginning of the list.
 +
 .Request
@@ -1860,7 +1860,7 @@
   ]
 ----
 
-Skip(s)::
+Skip(S)::
 Skip the given number of tags from the beginning of the list.
 +
 .Request
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 551777a..256be82 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
@@ -16,7 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.truth.TruthJUnit.assume;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.MoreObjects;
@@ -208,10 +207,6 @@
    */
   public static void init(Description desc, Config baseConfig, Path site) throws Exception {
     checkArgument(!desc.memory(), "can't initialize site path for in-memory test: %s", desc);
-    assume()
-        .withMessage("FUSED mode not yet supported for on-disk sites")
-        .that(NoteDbMode.get())
-        .isNotEqualTo(NoteDbMode.FUSED);
     Config cfg = desc.buildConfig(baseConfig);
     Map<String, Config> pluginConfigs = desc.buildPluginConfigs();
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/plugin/PluginIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/plugin/PluginIT.java
index 61aba93..3e1b2cb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/plugin/PluginIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/plugin/PluginIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.stream.Collectors.toList;
+import static org.junit.Assert.fail;
 
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
@@ -27,7 +28,9 @@
 import com.google.gerrit.extensions.api.plugins.Plugins.ListRequest;
 import com.google.gerrit.extensions.common.InstallPluginInput;
 import com.google.gerrit.extensions.common.PluginInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.RawInput;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import java.util.List;
@@ -35,10 +38,16 @@
 
 @NoHttpd
 public class PluginIT extends AbstractDaemonTest {
-  private static final byte[] JS_PLUGIN_CONTENT =
-      "Gerrit.install(function(self){});\n".getBytes(UTF_8);
+  private static final String JS_PLUGIN = "Gerrit.install(function(self){});\n";
+  private static final String HTML_PLUGIN =
+      String.format("<dom-module id=\"test\"><script>%s</script></dom-module>", JS_PLUGIN);
+  private static final RawInput JS_PLUGIN_CONTENT = RawInputUtil.create(JS_PLUGIN.getBytes(UTF_8));
+  private static final RawInput HTML_PLUGIN_CONTENT =
+      RawInputUtil.create(HTML_PLUGIN.getBytes(UTF_8));
+
   private static final List<String> PLUGINS =
-      ImmutableList.of("plugin-a", "plugin-b", "plugin-c", "plugin-d");
+      ImmutableList.of(
+          "plugin-a.js", "plugin-b.html", "plugin-c.js", "plugin-d.html", "plugin_e.js");
 
   @Test
   @GerritConfig(name = "plugins.allowRemoteAdmin", value = "true")
@@ -50,12 +59,15 @@
     PluginApi api;
     // Install all the plugins
     InstallPluginInput input = new InstallPluginInput();
-    input.raw = RawInputUtil.create(JS_PLUGIN_CONTENT);
     for (String plugin : PLUGINS) {
-      api = gApi.plugins().install(plugin + ".js", input);
+      input.raw = plugin.endsWith(".js") ? JS_PLUGIN_CONTENT : HTML_PLUGIN_CONTENT;
+      api = gApi.plugins().install(plugin, input);
       assertThat(api).isNotNull();
       PluginInfo info = api.get();
-      assertThat(info.id).isEqualTo(plugin);
+      String name = pluginName(plugin);
+      assertThat(info.id).isEqualTo(name);
+      assertThat(info.version).isEqualTo(pluginVersion(plugin));
+      assertThat(info.indexUrl).isEqualTo(String.format("plugins/%s/", name));
       assertThat(info.disabled).isNull();
     }
     assertPlugins(list().get(), PLUGINS);
@@ -63,6 +75,24 @@
     // With pagination
     assertPlugins(list().start(1).limit(2).get(), PLUGINS.subList(1, 3));
 
+    // With prefix
+    assertPlugins(list().prefix("plugin-b").get(), ImmutableList.of("plugin-b.html"));
+    assertPlugins(list().prefix("PLUGIN-").get(), ImmutableList.of());
+
+    // With substring
+    assertPlugins(list().substring("lugin-").get(), PLUGINS.subList(0, PLUGINS.size() - 1));
+    assertPlugins(list().substring("lugin-").start(1).limit(2).get(), PLUGINS.subList(1, 3));
+
+    // With regex
+    assertPlugins(list().regex(".*in-b").get(), ImmutableList.of("plugin-b.html"));
+    assertPlugins(list().regex("plugin-.*").get(), PLUGINS.subList(0, PLUGINS.size() - 1));
+    assertPlugins(list().regex("plugin-.*").start(1).limit(2).get(), PLUGINS.subList(1, 3));
+
+    // Invalid match combinations
+    assertBadRequest(list().regex(".*in-b").substring("a"));
+    assertBadRequest(list().regex(".*in-b").prefix("a"));
+    assertBadRequest(list().substring(".*in-b").prefix("a"));
+
     // Disable
     api = gApi.plugins().name("plugin-a");
     api.disable();
@@ -97,6 +127,28 @@
 
   private void assertPlugins(List<PluginInfo> actual, List<String> expected) {
     List<String> _actual = actual.stream().map(p -> p.id).collect(toList());
-    assertThat(_actual).containsExactlyElementsIn(expected);
+    List<String> _expected = expected.stream().map(p -> pluginName(p)).collect(toList());
+    assertThat(_actual).containsExactlyElementsIn(_expected);
+  }
+
+  private String pluginName(String plugin) {
+    int dot = plugin.indexOf(".");
+    assertThat(dot).isGreaterThan(0);
+    return plugin.substring(0, dot);
+  }
+
+  private String pluginVersion(String plugin) {
+    String name = pluginName(plugin);
+    int dash = name.lastIndexOf("-");
+    return dash > 0 ? name.substring(dash + 1) : "";
+  }
+
+  private void assertBadRequest(ListRequest req) throws Exception {
+    try {
+      req.get();
+      fail("Expected BadRequestException");
+    } catch (BadRequestException e) {
+      // Expected
+    }
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index d64d67f..8e3aeaf 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -748,7 +748,7 @@
 
   @Test
   public void retrySubmitAfterTornTopicOnLockFailure() throws Exception {
-    assume().that(notesMigration.fuseUpdates()).isTrue();
+    assume().that(notesMigration.disableChangeReviewDb()).isTrue();
 
     TestRepository<?> superRepo = createProjectWithPush("super-project");
     TestRepository<?> sub1 = createProjectWithPush("sub1");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
index 29a9a1f..281e6cd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/pgm/StandaloneNoteDbMigrationIT.java
@@ -105,7 +105,7 @@
     setUpOneChange();
 
     migrate("--trial", "false");
-    assertNotesMigrationState(NotesMigrationState.NOTE_DB_UNFUSED);
+    assertNotesMigrationState(NotesMigrationState.NOTE_DB);
 
     try (ServerContext ctx = startServer()) {
       GitRepositoryManager repoManager = ctx.getInjector().getInstance(GitRepositoryManager.class);
@@ -143,7 +143,7 @@
     assertServerStartupFails();
 
     migrate("--trial", "false");
-    assertNotesMigrationState(NotesMigrationState.NOTE_DB_UNFUSED);
+    assertNotesMigrationState(NotesMigrationState.NOTE_DB);
 
     status = new GerritIndexStatus(sitePaths);
     assertThat(status.getReady(ChangeSchemaDefinitions.NAME, version)).isTrue();
@@ -175,7 +175,7 @@
       u.runUpgrades();
 
       assertThat(indexes.getSearchIndex().getSchema().getVersion()).isEqualTo(currVersion);
-      assertNotesMigrationState(NotesMigrationState.NOTE_DB_UNFUSED);
+      assertNotesMigrationState(NotesMigrationState.NOTE_DB);
     }
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 77b70d3..6cbc532 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -916,7 +916,7 @@
 
   @Test
   public void retrySubmitSingleChangeOnLockFailure() throws Exception {
-    assume().that(notesMigration.fuseUpdates()).isTrue();
+    assume().that(notesMigration.disableChangeReviewDb()).isTrue();
 
     PushOneCommit.Result change = createChange();
     String id = change.getChangeId();
@@ -943,7 +943,7 @@
 
   @Test
   public void retrySubmitAfterTornTopicOnLockFailure() throws Exception {
-    assume().that(notesMigration.fuseUpdates()).isTrue();
+    assume().that(notesMigration.disableChangeReviewDb()).isTrue();
     assume().that(isSubmitWholeTopicEnabled()).isTrue();
 
     String topic = "test-topic";
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
new file mode 100644
index 0000000..b880152
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/PrivateByDefaultIT.java
@@ -0,0 +1,65 @@
+// 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.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.GerritConfig;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeInput;
+import org.junit.Test;
+
+public class PrivateByDefaultIT extends AbstractDaemonTest {
+  @Test
+  @GerritConfig(name = "change.privateByDefault", value = "true")
+  public void createChangeWithPrivateByDefaultEnabled() throws Exception {
+    ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
+    assertThat(gApi.changes().create(input).get().isPrivate).isEqualTo(true);
+  }
+
+  @Test
+  @GerritConfig(name = "change.privateByDefault", value = "true")
+  public void createChangeBypassPrivateByDefaultEnabled() throws Exception {
+    ChangeInput input = new ChangeInput(project.get(), "master", "empty change");
+    input.isPrivate = false;
+    assertThat(gApi.changes().create(input).get().isPrivate).isNull();
+  }
+
+  @Test
+  public void createChangeWithPrivateByDefaultDisabled() throws Exception {
+    ChangeInfo info =
+        gApi.changes().create(new ChangeInput(project.get(), "master", "empty change")).get();
+    assertThat(info.isPrivate).isNull();
+  }
+
+  @Test
+  @GerritConfig(name = "change.privateByDefault", value = "true")
+  public void pushWithPrivateByDefaultEnabled() throws Exception {
+    assertThat(createChange().getChange().change().isPrivate()).isEqualTo(true);
+  }
+
+  @Test
+  @GerritConfig(name = "change.privateByDefault", value = "true")
+  public void pushBypassPrivateByDefaultEnabled() throws Exception {
+    assertThat(createChange("refs/for/master%remove-private").getChange().change().isPrivate())
+        .isEqualTo(false);
+  }
+
+  @Test
+  public void pushWithPrivateByDefaultDisabled() throws Exception {
+    assertThat(createChange().getChange().change().isPrivate()).isEqualTo(false);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
index 3f675ef..41f7d4a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/config/FlushCacheIT.java
@@ -15,11 +15,13 @@
 package com.google.gerrit.acceptance.rest.config;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.config.ListCaches.CacheInfo;
 import org.junit.Test;
 
@@ -27,15 +29,20 @@
 
   @Test
   public void flushCache() throws Exception {
-    RestResponse r = adminRestSession.get("/config/server/caches/groups");
+    AccountGroup group = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    assertWithMessage("Precondition: The group 'Administrators' was loaded by the group cache")
+        .that(group)
+        .isNotNull();
+
+    RestResponse r = adminRestSession.get("/config/server/caches/groups_byname");
     CacheInfo result = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(result.entries.mem).isGreaterThan((long) 0);
 
-    r = adminRestSession.post("/config/server/caches/groups/flush");
+    r = adminRestSession.post("/config/server/caches/groups_byname/flush");
     r.assertOK();
     r.consume();
 
-    r = adminRestSession.get("/config/server/caches/groups");
+    r = adminRestSession.get("/config/server/caches/groups_byname");
     result = newGson().fromJson(r.getReader(), CacheInfo.class);
     assertThat(result.entries.mem).isNull();
   }
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 9efbe36..2cd1800 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
@@ -28,7 +28,6 @@
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
-import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.BatchUpdateListener;
@@ -77,7 +76,7 @@
 
   @Test
   public void updateChangeFailureRollsBackRefUpdate() throws Exception {
-    assume().that(notesMigration.fuseUpdates()).isTrue();
+    assume().that(notesMigration.disableChangeReviewDb()).isTrue();
     PushOneCommit.Result r = createChange();
     Change.Id id = r.getChange().getId();
 
@@ -149,7 +148,7 @@
 
   @Test
   public void retryOnLockFailureWithAtomicUpdates() throws Exception {
-    assume().that(notesMigration.fuseUpdates()).isTrue();
+    assume().that(notesMigration.disableChangeReviewDb()).isTrue();
     PushOneCommit.Result r = createChange();
     Change.Id id = r.getChange().getId();
     String master = "refs/heads/master";
@@ -195,52 +194,6 @@
     }
   }
 
-  @Test
-  public void noRetryOnLockFailureWithoutAtomicUpdates() throws Exception {
-    assume().that(notesMigration.fuseUpdates()).isFalse();
-
-    PushOneCommit.Result r = createChange();
-    Change.Id id = r.getChange().getId();
-    String master = "refs/heads/master";
-    ObjectId initial;
-    try (Repository repo = repoManager.openRepository(project)) {
-      initial = repo.exactRef(master).getObjectId();
-    }
-
-    AtomicInteger updateRepoCalledCount = new AtomicInteger();
-    AtomicInteger updateChangeCalledCount = new AtomicInteger();
-    AtomicInteger afterUpdateReposCalledCount = new AtomicInteger();
-
-    try {
-      retryHelper.execute(
-          batchUpdateFactory -> {
-            try (BatchUpdate bu = newBatchUpdate(batchUpdateFactory)) {
-              bu.addOp(
-                  id, new UpdateRefAndAddMessageOp(updateRepoCalledCount, updateChangeCalledCount));
-              bu.execute(new ConcurrentWritingListener(afterUpdateReposCalledCount));
-            }
-            return null;
-          });
-      assert_().fail("expected RestApiException");
-    } catch (RestApiException e) {
-      // Expected.
-    }
-
-    assertThat(updateRepoCalledCount.get()).isEqualTo(1);
-    assertThat(afterUpdateReposCalledCount.get()).isEqualTo(1);
-    assertThat(updateChangeCalledCount.get()).isEqualTo(0);
-
-    // updateChange was never called, so no message was ever added.
-    assertThat(getMessages(id)).doesNotContain(UpdateRefAndAddMessageOp.CHANGE_MESSAGE);
-
-    try (Repository repo = repoManager.openRepository(project)) {
-      // Op lost the race, so the other writer's commit happened first. Op didn't retry, because the
-      // ref updates weren't atomic, so it didn't throw LockFailureException on failure.
-      assertThat(commitMessages(repo, initial, repo.exactRef(master).getObjectId()))
-          .containsExactly(ConcurrentWritingListener.MSG_PREFIX + "1");
-    }
-  }
-
   private class ConcurrentWritingListener implements BatchUpdateListener {
     static final String MSG_PREFIX = "Other writer ";
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
index 485bb1b..f08a16e 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/OnlineNoteDbMigrationIT.java
@@ -17,7 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth8.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
-import static com.google.gerrit.server.notedb.NotesMigrationState.NOTE_DB_UNFUSED;
+import static com.google.gerrit.server.notedb.NotesMigrationState.NOTE_DB;
 import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_NO_SEQUENCE;
 import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY;
 import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY;
@@ -313,7 +313,7 @@
     Change.Id id2 = r2.getChange().getId();
 
     migrate(b -> b.setThreads(threads));
-    assertNotesMigrationState(NOTE_DB_UNFUSED);
+    assertNotesMigrationState(NOTE_DB);
 
     assertThat(sequences.nextChangeId()).isEqualTo(503);
 
@@ -372,7 +372,7 @@
     assertThat(NoteDbMigrator.getAutoMigrate(gerritConfig)).isTrue();
 
     migrate(b -> b);
-    assertNotesMigrationState(NOTE_DB_UNFUSED);
+    assertNotesMigrationState(NOTE_DB);
     assertThat(NoteDbMigrator.getAutoMigrate(gerritConfig)).isFalse();
   }
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
index cf4cfcd..774e4ed 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
@@ -14,27 +14,24 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupById;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import java.util.List;
 
 public class GroupDetail {
-  public AccountGroup group;
-  public List<AccountGroupMember> members;
-  public List<AccountGroupById> includes;
+  private List<AccountGroupMember> members;
+  private List<AccountGroupById> includes;
 
-  public GroupDetail() {}
-
-  public void setGroup(AccountGroup g) {
-    group = g;
+  public GroupDetail(List<AccountGroupMember> members, List<AccountGroupById> includes) {
+    this.members = members;
+    this.includes = includes;
   }
 
-  public void setMembers(List<AccountGroupMember> m) {
-    members = m;
+  public List<AccountGroupMember> getMembers() {
+    return members;
   }
 
-  public void setIncludes(List<AccountGroupById> i) {
-    includes = i;
+  public List<AccountGroupById> getIncludes() {
+    return includes;
   }
 }
diff --git a/gerrit-elasticsearch/BUILD b/gerrit-elasticsearch/BUILD
index 0affa12..ccaee55 100644
--- a/gerrit-elasticsearch/BUILD
+++ b/gerrit-elasticsearch/BUILD
@@ -5,7 +5,6 @@
     deps = [
         "//gerrit-antlr:query_exception",
         "//gerrit-extension-api:api",
-        "//gerrit-reviewdb:client",
         "//gerrit-reviewdb:server",
         "//gerrit-server:server",
         "//lib:gson",
@@ -36,7 +35,7 @@
     deps = [
         ":elasticsearch",
         "//gerrit-extension-api:api",
-        "//gerrit-reviewdb:client",
+        "//gerrit-reviewdb:server",
         "//gerrit-server:server",
         "//lib:gson",
         "//lib:guava",
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/plugins/Plugins.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/plugins/Plugins.java
index ed0d7f6..2828db5 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/plugins/Plugins.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/plugins/Plugins.java
@@ -35,6 +35,9 @@
     private boolean all;
     private int limit;
     private int start;
+    private String substring;
+    private String prefix;
+    private String regex;
 
     public List<PluginInfo> get() throws RestApiException {
       Map<String, PluginInfo> map = getAsMap();
@@ -73,6 +76,33 @@
     public int getStart() {
       return start;
     }
+
+    public ListRequest substring(String substring) {
+      this.substring = substring;
+      return this;
+    }
+
+    public String getSubstring() {
+      return substring;
+    }
+
+    public ListRequest prefix(String prefix) {
+      this.prefix = prefix;
+      return this;
+    }
+
+    public String getPrefix() {
+      return prefix;
+    }
+
+    public ListRequest regex(String regex) {
+      this.regex = regex;
+      return this;
+    }
+
+    public String getRegex() {
+      return regex;
+    }
   }
 
   /**
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
index 8ab5fbd..19d503f 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -338,6 +338,8 @@
       case REJECTED:
       case REJECTED_CURRENT_BRANCH:
       case RENAMED:
+      case REJECTED_MISSING_OBJECT:
+      case REJECTED_OTHER_REASON:
       default:
         break;
     }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
index 779d5d4..baf5a58 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
@@ -88,6 +88,8 @@
         case REJECTED:
         case REJECTED_CURRENT_BRANCH:
         case RENAMED:
+        case REJECTED_MISSING_OBJECT:
+        case REJECTED_OTHER_REASON:
         default:
           throw new ResourceConflictException("Failed to delete public key: " + saveResult);
       }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 41fcd04..7d1aceed 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -236,6 +236,8 @@
         case REJECTED:
         case REJECTED_CURRENT_BRANCH:
         case RENAMED:
+        case REJECTED_MISSING_OBJECT:
+        case REJECTED_OTHER_REASON:
         default:
           // TODO(dborowitz): Backoff and retry on LOCK_FAILURE.
           throw new ResourceConflictException("Failed to save public keys: " + saveResult);
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
index 39e2cb4..04ed1de 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
@@ -323,6 +323,8 @@
       case REJECTED:
       case REJECTED_CURRENT_BRANCH:
       case RENAMED:
+      case REJECTED_MISSING_OBJECT:
+      case REJECTED_OTHER_REASON:
       default:
         throw new AssertionError(result);
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
index b0b1e35..e4f5e576 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.java
@@ -43,6 +43,7 @@
     "description",
     "followup",
     "hashtags",
+    "move",
     "publish",
     "rebase",
     "restore",
@@ -58,6 +59,7 @@
   private static final Binder uiBinder = GWT.create(Binder.class);
 
   @UiField Button cherrypick;
+  @UiField Button move;
   @UiField Button rebase;
   @UiField Button revert;
   @UiField Button submit;
@@ -124,6 +126,7 @@
     if (hasUser) {
       a2b(actions, "abandon", abandon);
       a2b(actions, "/", deleteChange);
+      a2b(actions, "move", move);
       a2b(actions, "restore", restore);
       a2b(actions, "revert", revert);
       a2b(actions, "followup", followUp);
@@ -236,6 +239,11 @@
     CherryPickAction.call(cherrypick, changeInfo, revision, project, message);
   }
 
+  @UiHandler("move")
+  void onMove(@SuppressWarnings("unused") ClickEvent e) {
+    MoveAction.call(move, changeInfo, project);
+  }
+
   @UiHandler("revert")
   void onRevert(@SuppressWarnings("unused") ClickEvent e) {
     RevertAction.call(revert, changeId, project, revision, subject);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
index 60efc8c..8aeba90 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Actions.ui.xml
@@ -63,6 +63,9 @@
     <g:Button ui:field='cherrypick' styleName='' visible='false'>
       <div><ui:msg>Cherry Pick</ui:msg></div>
     </g:Button>
+    <g:Button ui:field='move' styleName='' visible='false'>
+      <div><ui:msg>Move Change</ui:msg></div>
+    </g:Button>
     <g:Button ui:field='rebase' styleName='' visible='false'>
       <div><ui:msg>Rebase</ui:msg></div>
     </g:Button>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/MoveAction.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/MoveAction.java
new file mode 100644
index 0000000..e3e9525
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/MoveAction.java
@@ -0,0 +1,67 @@
+// 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.client.change;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.info.ChangeInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.MoveDialog;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.PopupPanel;
+
+class MoveAction {
+  static void call(Button b, ChangeInfo info, Project.NameKey project) {
+    b.setEnabled(false);
+    new MoveDialog(project) {
+      {
+        sendButton.setText(Util.C.moveChangeSend());
+      }
+
+      @Override
+      public void onSend() {
+        ChangeApi.move(
+            info.project(),
+            info.legacyId().get(),
+            getDestinationBranch(),
+            getMessageText(),
+            new GerritCallback<ChangeInfo>() {
+              @Override
+              public void onSuccess(ChangeInfo result) {
+                sent = true;
+                hide();
+                Gerrit.display(PageLinks.toChange(project, result.legacyId()));
+              }
+
+              @Override
+              public void onFailure(Throwable caught) {
+                enableButtons(true);
+                super.onFailure(caught);
+              }
+            });
+      }
+
+      @Override
+      public void onClose(CloseEvent<PopupPanel> event) {
+        super.onClose(event);
+        b.setEnabled(true);
+      }
+    }.center();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
index b1f7561..c353e6f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -228,6 +228,15 @@
     call(project, id, commit, "cherrypick").post(cherryPickInput, cb);
   }
 
+  /** Move change to another branch. */
+  public static void move(
+      String project, int id, String destination, String message, AsyncCallback<ChangeInfo> cb) {
+    MoveInput moveInput = MoveInput.create();
+    moveInput.setMessage(message);
+    moveInput.setDestinationBranch(destination);
+    change(project, id).view("move").post(moveInput, cb);
+  }
+
   /** Edit commit message for specific revision of a change. */
   public static void message(
       @Nullable String project,
@@ -356,6 +365,18 @@
     protected CherryPickInput() {}
   }
 
+  private static class MoveInput extends JavaScriptObject {
+    static MoveInput create() {
+      return (MoveInput) createObject();
+    }
+
+    final native void setDestinationBranch(String d) /*-{ this.destination_branch = d; }-*/;
+
+    final native void setMessage(String m) /*-{ this.message = m; }-*/;
+
+    protected MoveInput() {}
+  }
+
   private static class PrivateInput extends JavaScriptObject {
     static PrivateInput create() {
       return (PrivateInput) createObject();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 80049df..402179c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -145,6 +145,14 @@
 
   String cherryPickTitle();
 
+  String moveChangeSend();
+
+  String headingMoveBranch();
+
+  String moveChangeMessage();
+
+  String moveTitle();
+
   String buttonRebaseChangeSend();
 
   String rebaseConfirmMessage();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index 8a9f323..dd11a60 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -78,6 +78,11 @@
 cherryPickCommitMessage = Cherry Pick Commit Message:
 cherryPickTitle = Code Review - Cherry Pick Change to Another Branch
 
+headingMoveBranch = Move Change to Branch:
+moveChangeSend = Move Change
+moveChangeMessage = Move Change Message:
+moveTitle = Code Review - Move Change to Another Branch
+
 buttonRebaseChangeSend = Rebase
 rebaseConfirmMessage = Change parent revision
 rebaseNotPossibleMessage = Change is already up to date
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index 181fd73..73cc995 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -49,7 +49,7 @@
       Project.NameKey name, String viewName, int limit, int start, String match) {
     RestApi call = project(name).view(viewName);
     call.addParameter("n", limit);
-    call.addParameter("s", start);
+    call.addParameter("S", start);
     if (match != null) {
       if (match.startsWith("^")) {
         call.addParameter("r", match);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MoveDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MoveDialog.java
new file mode 100644
index 0000000..3821e93
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MoveDialog.java
@@ -0,0 +1,107 @@
+// 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.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.projects.BranchInfo;
+import com.google.gerrit.client.projects.ProjectApi;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.SuggestBox;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
+import java.util.ArrayList;
+import java.util.List;
+
+public abstract class MoveDialog extends TextAreaActionDialog {
+  private SuggestBox newBranch;
+  private List<BranchInfo> branches;
+
+  public MoveDialog(Project.NameKey project) {
+    super(Util.C.moveTitle(), Util.C.moveChangeMessage());
+    ProjectApi.getBranches(
+        project,
+        new GerritCallback<JsArray<BranchInfo>>() {
+          @Override
+          public void onSuccess(JsArray<BranchInfo> result) {
+            branches = Natives.asList(result);
+          }
+        });
+
+    newBranch =
+        new SuggestBox(
+            new HighlightSuggestOracle() {
+              @Override
+              protected void onRequestSuggestions(Request request, Callback done) {
+                List<BranchSuggestion> suggestions = new ArrayList<>();
+                for (BranchInfo b : branches) {
+                  if (b.ref().contains(request.getQuery())) {
+                    suggestions.add(new BranchSuggestion(b));
+                  }
+                }
+                done.onSuggestionsReady(request, new Response(suggestions));
+              }
+            });
+
+    newBranch.setWidth("100%");
+    newBranch.getElement().getStyle().setProperty("boxSizing", "border-box");
+    message.setCharacterWidth(70);
+
+    FlowPanel mwrap = new FlowPanel();
+    mwrap.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
+    mwrap.add(newBranch);
+
+    panel.insert(mwrap, 0);
+    panel.insert(new SmallHeading(Util.C.headingMoveBranch()), 0);
+  }
+
+  @Override
+  public void center() {
+    super.center();
+    GlobalKey.dialog(this);
+    newBranch.setFocus(true);
+  }
+
+  public String getDestinationBranch() {
+    return newBranch.getText();
+  }
+
+  static class BranchSuggestion implements Suggestion {
+    private BranchInfo branch;
+
+    BranchSuggestion(BranchInfo branch) {
+      this.branch = branch;
+    }
+
+    @Override
+    public String getDisplayString() {
+      String refsHeads = "refs/heads/";
+      if (branch.ref().startsWith(refsHeads)) {
+        return branch.ref().substring(refsHeads.length());
+      }
+      return branch.ref();
+    }
+
+    @Override
+    public String getReplacementString() {
+      return branch.getShortName();
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/doc/QueryDocumentationFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/QueryDocumentationFilter.java
similarity index 98%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/doc/QueryDocumentationFilter.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/QueryDocumentationFilter.java
index 1604997..7a89b3b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/doc/QueryDocumentationFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/QueryDocumentationFilter.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd.rpc.doc;
+package com.google.gerrit.httpd;
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableListMultimap;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index e705fbd..1d116b7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.httpd.restapi.ConfigRestApiServlet;
 import com.google.gerrit.httpd.restapi.GroupsRestApiServlet;
 import com.google.gerrit.httpd.restapi.ProjectsRestApiServlet;
-import com.google.gerrit.httpd.rpc.doc.QueryDocumentationFilter;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.AuthConfig;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java
index fd825b8..c34b423 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/api/VersionedMetaDataOnInit.java
@@ -128,6 +128,8 @@
       case REJECTED:
       case REJECTED_CURRENT_BRANCH:
       case RENAMED:
+      case REJECTED_MISSING_OBJECT:
+      case REJECTED_OTHER_REASON:
       default:
         throw new IOException(
             "Failed to update " + getRefName() + " of " + project + ": " + r.name());
diff --git a/gerrit-plugin-api/BUILD b/gerrit-plugin-api/BUILD
index cc01802..d8d6838 100644
--- a/gerrit-plugin-api/BUILD
+++ b/gerrit-plugin-api/BUILD
@@ -20,6 +20,7 @@
     "//gerrit-gwtexpui:server",
     "//gerrit-reviewdb:server",
     "//gerrit-server:prolog-common",
+    "//lib/commons:dbcp",
     "//lib/commons:lang",
     "//lib/commons:lang3",
     "//lib/dropwizard:dropwizard-core",
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java
index ff7a5ce..731d156 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeFinder.java
@@ -15,12 +15,14 @@
 package com.google.gerrit.server;
 
 import com.google.common.base.Throwables;
+import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import com.google.common.primitives.Ints;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.change.ChangeTriplet;
 import com.google.gerrit.server.index.IndexConfig;
 import com.google.gerrit.server.project.ChangeControl;
@@ -29,8 +31,10 @@
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Module;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.google.inject.name.Named;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -40,8 +44,19 @@
 
 @Singleton
 public class ChangeFinder {
+  private static final String CACHE_NAME = "changeid_project";
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        cache(CACHE_NAME, Change.Id.class, String.class).maximumWeight(1024);
+      }
+    };
+  }
 
   private final IndexConfig indexConfig;
+  private final Cache<Change.Id, String> changeIdProjectCache;
   private final Provider<InternalChangeQuery> queryProvider;
   private final Provider<ReviewDb> reviewDb;
   private final ChangeControl.GenericFactory changeControlFactory;
@@ -49,10 +64,12 @@
   @Inject
   ChangeFinder(
       IndexConfig indexConfig,
+      @Named(CACHE_NAME) Cache<Change.Id, String> changeIdProjectCache,
       Provider<InternalChangeQuery> queryProvider,
       Provider<ReviewDb> reviewDb,
       ChangeControl.GenericFactory changeControlFactory) {
     this.indexConfig = indexConfig;
+    this.changeIdProjectCache = changeIdProjectCache;
     this.queryProvider = queryProvider;
     this.reviewDb = reviewDb;
     this.changeControlFactory = changeControlFactory;
@@ -72,69 +89,69 @@
       return Collections.emptyList();
     }
 
+    int z = id.lastIndexOf('~');
+    int y = id.lastIndexOf('~', z - 1);
+    if (y < 0 && z > 0) {
+      // Try project~numericChangeId
+      Integer n = Ints.tryParse(id.substring(z + 1));
+      if (n != null) {
+        return fromProjectNumber(user, id.substring(0, z), n.intValue());
+      }
+    }
+
+    if (y < 0 && z < 0) {
+      // Try numeric changeId
+      Integer n = Ints.tryParse(id);
+      if (n != null) {
+        return find(new Change.Id(n), user);
+      }
+    }
+
     // Use the index to search for changes, but don't return any stored fields,
     // to force rereading in case the index is stale.
     InternalChangeQuery query = queryProvider.get().noFields();
-
-    int numTwiddles = 0;
-    for (char c : id.toCharArray()) {
-      if (c == '~') {
-        numTwiddles++;
-      }
-    }
-
-    if (numTwiddles == 1) {
-      // Try project~numericChangeId
-      String project = id.substring(0, id.indexOf('~'));
-      Integer n = Ints.tryParse(id.substring(project.length() + 1));
-      if (n != null) {
-        Change.Id changeId = new Change.Id(n);
-        try {
-          return ImmutableList.of(
-              changeControlFactory.controlFor(
-                  reviewDb.get(), Project.NameKey.parse(project), changeId, user));
-        } catch (NoSuchChangeException e) {
-          return Collections.emptyList();
-        } catch (IllegalArgumentException e) {
-          String changeNotFound = String.format("change %s not found in ReviewDb", changeId);
-          String projectNotFound =
-              String.format(
-                  "passed project %s when creating ChangeNotes for %s, but actual project is",
-                  project, changeId);
-          if (e.getMessage().equals(changeNotFound) || e.getMessage().startsWith(projectNotFound)) {
-            return Collections.emptyList();
-          }
-          throw e;
-        } catch (OrmException e) {
-          // Distinguish between a RepositoryNotFoundException (project argument invalid) and
-          // other OrmExceptions (failure in the persistence layer).
-          if (Throwables.getRootCause(e) instanceof RepositoryNotFoundException) {
-            return Collections.emptyList();
-          }
-          throw e;
-        }
-      }
-    } else if (numTwiddles == 2) {
-      // Try change triplet
-      Optional<ChangeTriplet> triplet = ChangeTriplet.parse(id);
+    if (y > 0 && z > 0) {
+      // Try change triplet (project~branch~Ihash...)
+      Optional<ChangeTriplet> triplet = ChangeTriplet.parse(id, y, z);
       if (triplet.isPresent()) {
-        return asChangeControls(
-            query.byBranchKey(triplet.get().branch(), triplet.get().id()), user);
+        ChangeTriplet t = triplet.get();
+        return asChangeControls(query.byBranchKey(t.branch(), t.id()), user);
       }
     }
 
-    // Try numeric changeId
-    if (id.charAt(0) != '0') {
-      Integer n = Ints.tryParse(id);
-      if (n != null) {
-        return asChangeControls(query.byLegacyChangeId(new Change.Id(n)), user);
-      }
-    }
-
-    // Try isolated changeId
+    // Try isolated Ihash... format ("Change-Id: Ihash").
     return asChangeControls(query.byKeyPrefix(id), user);
   }
 
+  private List<ChangeControl> fromProjectNumber(CurrentUser user, String project, int changeNumber)
+      throws OrmException {
+    Change.Id cId = new Change.Id(changeNumber);
+    try {
+      return ImmutableList.of(
+          changeControlFactory.controlFor(
+              reviewDb.get(), Project.NameKey.parse(project), cId, user));
+    } catch (NoSuchChangeException e) {
+      return Collections.emptyList();
+    } catch (IllegalArgumentException e) {
+      String changeNotFound = String.format("change %s not found in ReviewDb", cId);
+      String projectNotFound =
+          String.format(
+              "passed project %s when creating ChangeNotes for %s, but actual project is",
+              project, cId);
+      if (e.getMessage().equals(changeNotFound) || e.getMessage().startsWith(projectNotFound)) {
+        return Collections.emptyList();
+      }
+      throw e;
+    } catch (OrmException e) {
+      // Distinguish between a RepositoryNotFoundException (project argument invalid) and
+      // other OrmExceptions (failure in the persistence layer).
+      if (Throwables.getRootCause(e) instanceof RepositoryNotFoundException) {
+        return Collections.emptyList();
+      }
+      throw e;
+    }
+  }
+
   public ChangeControl findOne(Change.Id id, CurrentUser user) throws OrmException {
     List<ChangeControl> ctls = find(id, user);
     if (ctls.size() != 1) {
@@ -144,10 +161,19 @@
   }
 
   public List<ChangeControl> find(Change.Id id, CurrentUser user) throws OrmException {
+    String project = changeIdProjectCache.getIfPresent(id);
+    if (project != null) {
+      return fromProjectNumber(user, project, id.get());
+    }
+
     // Use the index to search for changes, but don't return any stored fields,
     // to force rereading in case the index is stale.
     InternalChangeQuery query = queryProvider.get().noFields();
-    return asChangeControls(query.byLegacyChangeId(id), user);
+    List<ChangeData> r = query.byLegacyChangeId(id);
+    if (r.size() == 1) {
+      changeIdProjectCache.put(id, r.get(0).project().get());
+    }
+    return asChangeControls(r, user);
   }
 
   private List<ChangeControl> asChangeControls(List<ChangeData> cds, CurrentUser user)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
index 2199d3a..c1f0989 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -412,6 +412,8 @@
         case REJECTED:
         case REJECTED_CURRENT_BRANCH:
         case RENAMED:
+        case REJECTED_MISSING_OBJECT:
+        case REJECTED_OTHER_REASON:
         default:
           throw new OrmException(
               String.format("Update star labels on ref %s failed: %s", refName, result.name()));
@@ -439,6 +441,8 @@
       case REJECTED:
       case REJECTED_CURRENT_BRANCH:
       case RENAMED:
+      case REJECTED_MISSING_OBJECT:
+      case REJECTED_OTHER_REASON:
       default:
         throw new OrmException(
             String.format("Delete star ref %s failed: %s", refName, result.name()));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
index fb7d7e7..56df1d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.data.GroupDetail;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -34,20 +35,15 @@
 
   private final ReviewDb db;
   private final GroupControl.Factory groupControl;
-  private final GroupCache groupCache;
 
   private final AccountGroup.Id groupId;
   private GroupControl control;
 
   @Inject
   GroupDetailFactory(
-      ReviewDb db,
-      GroupControl.Factory groupControl,
-      GroupCache groupCache,
-      @Assisted AccountGroup.Id groupId) {
+      ReviewDb db, GroupControl.Factory groupControl, @Assisted AccountGroup.Id groupId) {
     this.db = db;
     this.groupControl = groupControl;
-    this.groupCache = groupCache;
 
     this.groupId = groupId;
   }
@@ -55,12 +51,9 @@
   @Override
   public GroupDetail call() throws OrmException, NoSuchGroupException {
     control = groupControl.validateFor(groupId);
-    AccountGroup group = groupCache.get(groupId);
-    GroupDetail detail = new GroupDetail();
-    detail.setGroup(group);
-    detail.setMembers(loadMembers());
-    detail.setIncludes(loadIncludes());
-    return detail;
+    List<AccountGroupMember> members = loadMembers();
+    List<AccountGroupById> includes = loadIncludes();
+    return new GroupDetail(members, includes);
   }
 
   private List<AccountGroupMember> loadMembers() throws OrmException {
@@ -74,14 +67,14 @@
   }
 
   private List<AccountGroupById> loadIncludes() throws OrmException {
-    List<AccountGroupById> groups = new ArrayList<>();
-
-    for (AccountGroupById m : db.accountGroupById().byGroup(groupId)) {
-      if (control.canSeeGroup()) {
-        groups.add(m);
-      }
+    if (!control.canSeeGroup()) {
+      return ImmutableList.of();
     }
 
+    List<AccountGroupById> groups = new ArrayList<>();
+    for (AccountGroupById m : db.accountGroupById().byGroup(groupId)) {
+      groups.add(m);
+    }
     return groups;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
index d84d051..9173bd2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
@@ -104,17 +104,13 @@
     final GroupDetail groupDetail = groupDetailFactory.create(group.getId()).call();
 
     final Set<Account> members = new HashSet<>();
-    if (groupDetail.members != null) {
-      for (AccountGroupMember member : groupDetail.members) {
-        members.add(accountCache.get(member.getAccountId()).getAccount());
-      }
+    for (AccountGroupMember member : groupDetail.getMembers()) {
+      members.add(accountCache.get(member.getAccountId()).getAccount());
     }
-    if (groupDetail.includes != null) {
-      for (AccountGroupById groupInclude : groupDetail.includes) {
-        final AccountGroup includedGroup = groupCache.get(groupInclude.getIncludeUUID());
-        if (includedGroup != null && !seen.contains(includedGroup.getGroupUUID())) {
-          members.addAll(listAccounts(includedGroup.getGroupUUID(), project, seen));
-        }
+    for (AccountGroupById groupInclude : groupDetail.getIncludes()) {
+      final AccountGroup includedGroup = groupCache.get(groupInclude.getIncludeUUID());
+      if (includedGroup != null && !seen.contains(includedGroup.getGroupUUID())) {
+        members.addAll(listAccounts(includedGroup.getGroupUUID(), project, seen));
       }
     }
     return members;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
index 2985504..46f27f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsUpdate.java
@@ -757,6 +757,8 @@
       case NOT_ATTEMPTED:
       case REJECTED:
       case REJECTED_CURRENT_BRANCH:
+      case REJECTED_MISSING_OBJECT:
+      case REJECTED_OTHER_REASON:
       default:
         throw new IOException("Updating external IDs failed with " + res);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
index d7f868c..84f4535 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/groups/GroupApiImpl.java
@@ -16,13 +16,11 @@
 
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
-import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.api.groups.GroupApi;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.GroupAuditEventInfo;
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.common.GroupOptionsInfo;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.group.AddIncludedGroups;
 import com.google.gerrit.server.group.AddMembers;
@@ -144,8 +142,6 @@
     in.name = name;
     try {
       putName.apply(rsrc, in);
-    } catch (NoSuchGroupException e) {
-      throw new ResourceNotFoundException(name, e);
     } catch (Exception e) {
       throw asRestApiException("Cannot put group name", e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/plugins/PluginsImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/plugins/PluginsImpl.java
index dead1ad..75fb350 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/plugins/PluginsImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/plugins/PluginsImpl.java
@@ -63,6 +63,9 @@
         list.setAll(this.getAll());
         list.setStart(this.getStart());
         list.setLimit(this.getLimit());
+        list.setMatchPrefix(this.getPrefix());
+        list.setMatchSubstring(this.getSubstring());
+        list.setMatchRegex(this.getRegex());
         return list.apply();
       }
     };
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
index 71a3db7..2daeb7c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeTriplet.java
@@ -38,20 +38,22 @@
    * @return the triplet if the input string has the proper format, or absent if not.
    */
   public static Optional<ChangeTriplet> parse(String triplet) {
-    int t2 = triplet.lastIndexOf('~');
-    int t1 = triplet.lastIndexOf('~', t2 - 1);
-    if (t1 < 0 || t2 < 0) {
+    int z = triplet.lastIndexOf('~');
+    int y = triplet.lastIndexOf('~', z - 1);
+    return parse(triplet, y, z);
+  }
+
+  public static Optional<ChangeTriplet> parse(String triplet, int y, int z) {
+    if (y < 0 || z < 0) {
       return Optional.empty();
     }
 
-    String project = Url.decode(triplet.substring(0, t1));
-    String branch = Url.decode(triplet.substring(t1 + 1, t2));
-    String changeId = Url.decode(triplet.substring(t2 + 1));
-
-    ChangeTriplet result =
+    String project = Url.decode(triplet.substring(0, y));
+    String branch = Url.decode(triplet.substring(y + 1, z));
+    String changeId = Url.decode(triplet.substring(z + 1));
+    return Optional.of(
         new AutoValue_ChangeTriplet(
-            new Branch.NameKey(new Project.NameKey(project), branch), new Change.Key(changeId));
-    return Optional.of(result);
+            new Branch.NameKey(new Project.NameKey(project), branch), new Change.Key(changeId)));
   }
 
   public final Project.NameKey project() {
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 9fcb13d..56f637e 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
@@ -616,6 +616,8 @@
         case REJECTED:
         case REJECTED_CURRENT_BRANCH:
         case RENAMED:
+        case REJECTED_MISSING_OBJECT:
+        case REJECTED_OTHER_REASON:
         default:
           p.status = Status.FIX_FAILED;
           p.outcome = "Failed to update patch set ref: " + result;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
index bbe04f5..92ad46d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateChange.java
@@ -105,6 +105,7 @@
   private final ChangeFinder changeFinder;
   private final PatchSetUtil psUtil;
   private final boolean allowDrafts;
+  private final boolean privateByDefault;
   private final MergeUtil.Factory mergeUtilFactory;
   private final SubmitType submitType;
   private final NotifyUtil notifyUtil;
@@ -143,6 +144,7 @@
     this.changeFinder = changeFinder;
     this.psUtil = psUtil;
     this.allowDrafts = config.getBoolean("change", "allowDrafts", true);
+    this.privateByDefault = config.getBoolean("change", "privateByDefault", false);
     this.submitType = config.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
     this.mergeUtilFactory = mergeUtilFactory;
     this.notifyUtil = notifyUtil;
@@ -258,7 +260,7 @@
       }
       ins.setTopic(topic);
       ins.setDraft(input.status == ChangeStatus.DRAFT);
-      ins.setPrivate(input.isPrivate != null && input.isPrivate);
+      ins.setPrivate(input.isPrivate == null ? privateByDefault : input.isPrivate);
       ins.setWorkInProgress(input.workInProgress != null && input.workInProgress);
       ins.setGroups(groups);
       ins.setNotify(input.notify);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
index ffc0dc4..f895bf7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Move.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.webui.UiAction;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
@@ -40,6 +41,7 @@
 import com.google.gerrit.server.permissions.ChangePermission;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
 import com.google.gerrit.server.permissions.RefPermission;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gerrit.server.update.BatchUpdate;
@@ -60,7 +62,8 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 @Singleton
-public class Move extends RetryingRestModifyView<ChangeResource, MoveInput, ChangeInfo> {
+public class Move extends RetryingRestModifyView<ChangeResource, MoveInput, ChangeInfo>
+    implements UiAction<ChangeResource> {
   private final PermissionBackend permissionBackend;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson.Factory json;
@@ -209,4 +212,16 @@
       return true;
     }
   }
+
+  @Override
+  public UiAction.Description getDescription(ChangeResource rsrc) {
+    return new UiAction.Description()
+        .setLabel("Move Change")
+        .setTitle("Move change to a different branch")
+        .setVisible(
+            permissionBackend
+                .user(rsrc.getUser())
+                .project(rsrc.getProject())
+                .testOrFalse(ProjectPermission.CREATE_CHANGE));
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 5a8945d..31989e3 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
@@ -74,6 +74,7 @@
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeFinder;
 import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PluginUser;
@@ -218,6 +219,7 @@
     install(AccountCacheImpl.module());
     install(BatchUpdate.module());
     install(ChangeKindCacheImpl.module());
+    install(ChangeFinder.module());
     install(ConflictsCacheImpl.module());
     install(GroupCacheImpl.module());
     install(GroupIncludeCacheImpl.module());
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 e51312b..0d84767 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
@@ -285,6 +285,8 @@
       case REJECTED:
       case REJECTED_CURRENT_BRANCH:
       case RENAMED:
+      case REJECTED_MISSING_OBJECT:
+      case REJECTED_OTHER_REASON:
       default:
         throw new IOException(String.format("Failed to delete ref %s: %s", refName, result));
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index ff5c5ed..ea28fa9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -63,7 +63,6 @@
 import com.google.gerrit.server.git.strategy.SubmitStrategyListener;
 import com.google.gerrit.server.git.validators.MergeValidationException;
 import com.google.gerrit.server.git.validators.MergeValidators;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.SubmitRuleOptions;
@@ -234,7 +233,6 @@
   private final Provider<MergeOpRepoManager> ormProvider;
   private final NotifyUtil notifyUtil;
   private final RetryHelper retryHelper;
-  private final NotesMigration notesMigration;
 
   private Timestamp ts;
   private RequestId submissionId;
@@ -262,8 +260,7 @@
       Provider<MergeOpRepoManager> ormProvider,
       NotifyUtil notifyUtil,
       TopicMetrics topicMetrics,
-      RetryHelper retryHelper,
-      NotesMigration notesMigration) {
+      RetryHelper retryHelper) {
     this.cmUtil = cmUtil;
     this.batchUpdateFactory = batchUpdateFactory;
     this.internalUserFactory = internalUserFactory;
@@ -276,7 +273,6 @@
     this.notifyUtil = notifyUtil;
     this.retryHelper = retryHelper;
     this.topicMetrics = topicMetrics;
-    this.notesMigration = notesMigration;
   }
 
   @Override
@@ -909,11 +905,6 @@
       return "Error submitting changes";
     }
     if (p == 1) {
-      if (!notesMigration.fuseUpdates()) {
-        // No fused updates: any subset of changes might or might not have been submitted, so don't
-        // make any strong claims.
-        return "Error submitting changes";
-      }
       // Fused updates: it's correct to say that none of the n changes were submitted.
       return "Error submitting " + c + " changes";
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 5cd2184..42fb1b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -2177,7 +2177,9 @@
           changeInserterFactory
               .create(changeId, commit, refName)
               .setTopic(magicBranch.topic)
-              .setPrivate(magicBranch.isPrivate)
+              .setPrivate(
+                  magicBranch.isPrivate
+                      || (receiveConfig.privateByDefault && !magicBranch.removePrivate))
               .setWorkInProgress(magicBranch.workInProgress)
               // Changes already validated in validateNewCommits.
               .setValidate(false);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
index 2b9f594..a3f2a31 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
@@ -28,6 +28,7 @@
   final boolean checkMagicRefs;
   final boolean checkReferencedObjectsAreReachable;
   final boolean allowDrafts;
+  final boolean privateByDefault;
   private final int systemMaxBatchChanges;
   private final AccountLimits.Factory limitsFactory;
 
@@ -37,6 +38,7 @@
     checkReferencedObjectsAreReachable =
         config.getBoolean("receive", null, "checkReferencedObjectsAreReachable", true);
     allowDrafts = config.getBoolean("change", null, "allowDrafts", true);
+    privateByDefault = config.getBoolean("change", null, "privateByDefault", false);
     systemMaxBatchChanges = config.getInt("receive", "maxBatchChanges", 0);
     this.limitsFactory = limitsFactory;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
index 99661b3..c7b64c0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -359,6 +359,8 @@
           case REJECTED:
           case REJECTED_CURRENT_BRANCH:
           case RENAMED:
+          case REJECTED_MISSING_OBJECT:
+          case REJECTED_OTHER_REASON:
           default:
             throw new IOException(
                 "Cannot delete "
@@ -444,6 +446,8 @@
           case REJECTED:
           case REJECTED_CURRENT_BRANCH:
           case RENAMED:
+          case REJECTED_MISSING_OBJECT:
+          case REJECTED_OTHER_REASON:
           default:
             throw new IOException(
                 "Cannot update "
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
index 040550c..afc3f48 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
@@ -99,15 +99,16 @@
     input = Input.init(input);
 
     GroupControl control = resource.getControl();
+    if (!control.canAddGroup()) {
+      throw new AuthException(String.format("Cannot add groups to group %s", group.getName()));
+    }
+
     Map<AccountGroup.UUID, AccountGroupById> newIncludedGroups = new HashMap<>();
     List<GroupInfo> result = new ArrayList<>();
     Account.Id me = control.getUser().getAccountId();
 
     for (String includedGroup : input.groups) {
       GroupDescription.Basic d = groupsCollection.parse(includedGroup);
-      if (!control.canAddGroup()) {
-        throw new AuthException(String.format("Cannot add group: %s", d.getName()));
-      }
 
       if (!newIncludedGroups.containsKey(d.getGroupUUID())) {
         AccountGroupById.Key agiKey = new AccountGroupById.Key(group.getId(), d.getGroupUUID());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
index 04be41e..4263bbe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
@@ -124,6 +124,9 @@
     input = Input.init(input);
 
     GroupControl control = resource.getControl();
+    if (!control.canAddMember()) {
+      throw new AuthException("Cannot add members to group " + internalGroup.getName());
+    }
 
     Set<Account.Id> newMemberIds = new HashSet<>();
     for (String nameOrEmailOrId : input.members) {
@@ -132,10 +135,6 @@
         throw new UnprocessableEntityException(
             String.format("Account Inactive: %s", nameOrEmailOrId));
       }
-
-      if (!control.canAddMember()) {
-        throw new AuthException("Cannot add member: " + a.getFullName());
-      }
       newMemberIds.add(a.getId());
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
index 1e1008c7a..c447daf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
@@ -71,16 +71,16 @@
     input = Input.init(input);
 
     final GroupControl control = resource.getControl();
+    if (!control.canRemoveGroup()) {
+      throw new AuthException(
+          String.format("Cannot delete groups from group %s", internalGroup.getName()));
+    }
+
     final Map<AccountGroup.UUID, AccountGroupById> includedGroups =
         getIncludedGroups(internalGroup.getId());
     final List<AccountGroupById> toRemove = new ArrayList<>();
-
     for (String includedGroup : input.groups) {
       GroupDescription.Basic d = groupsCollection.parse(includedGroup);
-      if (!control.canRemoveGroup()) {
-        throw new AuthException(String.format("Cannot delete group: %s", d.getName()));
-      }
-
       AccountGroupById g = includedGroups.remove(d.getGroupUUID());
       if (g != null) {
         toRemove.add(g);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
index 6be46d6..dd3e022 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
@@ -73,16 +73,14 @@
     input = Input.init(input);
 
     final GroupControl control = resource.getControl();
+    if (!control.canRemoveMember()) {
+      throw new AuthException("Cannot delete members from group " + internalGroup.getName());
+    }
+
     final Map<Account.Id, AccountGroupMember> members = getMembers(internalGroup.getId());
     final List<AccountGroupMember> toRemove = new ArrayList<>();
-
     for (String nameOrEmail : input.members) {
       Account a = accounts.parse(nameOrEmail).getAccount();
-
-      if (!control.canRemoveMember()) {
-        throw new AuthException("Cannot delete member: " + a.getFullName());
-      }
-
       final AccountGroupMember m = members.remove(a.getId());
       if (m != null) {
         toRemove.add(m);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java
index e29b37f..e2a467a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetAuditLog.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.extensions.common.GroupInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
-import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -64,18 +63,14 @@
 
   @Override
   public List<? extends GroupAuditEventInfo> apply(GroupResource rsrc)
-      throws AuthException, ResourceNotFoundException, MethodNotAllowedException, OrmException {
-    if (rsrc.toAccountGroup() == null) {
+      throws AuthException, MethodNotAllowedException, OrmException {
+    AccountGroup group = rsrc.toAccountGroup();
+    if (group == null) {
       throw new MethodNotAllowedException();
     } else if (!rsrc.getControl().isOwner()) {
       throw new AuthException("Not group owner");
     }
 
-    AccountGroup group = db.get().accountGroups().get(rsrc.toAccountGroup().getId());
-    if (group == null) {
-      throw new ResourceNotFoundException();
-    }
-
     AccountLoader accountLoader = accountLoaderFactory.create(true);
 
     List<GroupAuditEventInfo> auditEvents = new ArrayList<>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
index b24d094..84b7665 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
@@ -102,20 +102,16 @@
       return Collections.emptyMap();
     }
 
-    if (groupDetail.members != null) {
-      for (AccountGroupMember m : groupDetail.members) {
-        if (!members.containsKey(m.getAccountId())) {
-          members.put(m.getAccountId(), accountLoader.get(m.getAccountId()));
-        }
+    for (AccountGroupMember m : groupDetail.getMembers()) {
+      if (!members.containsKey(m.getAccountId())) {
+        members.put(m.getAccountId(), accountLoader.get(m.getAccountId()));
       }
     }
 
     if (recursive) {
-      if (groupDetail.includes != null) {
-        for (AccountGroupById includedGroup : groupDetail.includes) {
-          if (!seenGroups.contains(includedGroup.getIncludeUUID())) {
-            members.putAll(getMembers(includedGroup.getIncludeUUID(), seenGroups));
-          }
+      for (AccountGroupById includedGroup : groupDetail.getIncludes()) {
+        if (!seenGroups.contains(includedGroup.getIncludeUUID())) {
+          members.putAll(getMembers(includedGroup.getIncludeUUID(), seenGroups));
         }
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
index b78f4a5..bdf7a3e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
@@ -15,8 +15,7 @@
 package com.google.gerrit.server.group;
 
 import com.google.common.base.Strings;
-import com.google.gerrit.common.data.GroupDetail;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.common.collect.ImmutableList;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.DefaultInput;
@@ -28,7 +27,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.GroupDetailFactory;
 import com.google.gerrit.server.git.RenameGroupOp;
 import com.google.gerrit.server.group.PutName.Input;
 import com.google.gwtorm.server.OrmException;
@@ -36,7 +34,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.Collections;
 import java.util.Date;
 import java.util.TimeZone;
 import java.util.concurrent.Future;
@@ -50,7 +47,6 @@
 
   private final Provider<ReviewDb> db;
   private final GroupCache groupCache;
-  private final GroupDetailFactory.Factory groupDetailFactory;
   private final RenameGroupOp.Factory renameGroupOpFactory;
   private final Provider<IdentifiedUser> currentUser;
 
@@ -58,12 +54,10 @@
   PutName(
       Provider<ReviewDb> db,
       GroupCache groupCache,
-      GroupDetailFactory.Factory groupDetailFactory,
       RenameGroupOp.Factory renameGroupOpFactory,
       Provider<IdentifiedUser> currentUser) {
     this.db = db;
     this.groupCache = groupCache;
-    this.groupDetailFactory = groupDetailFactory;
     this.renameGroupOpFactory = renameGroupOpFactory;
     this.currentUser = currentUser;
   }
@@ -71,7 +65,7 @@
   @Override
   public String apply(GroupResource rsrc, Input input)
       throws MethodNotAllowedException, AuthException, BadRequestException,
-          ResourceConflictException, OrmException, NoSuchGroupException, IOException {
+          ResourceConflictException, OrmException, IOException {
     if (rsrc.toAccountGroup() == null) {
       throw new MethodNotAllowedException();
     } else if (!rsrc.getControl().isOwner()) {
@@ -88,25 +82,25 @@
       return newName;
     }
 
-    return renameGroup(rsrc.toAccountGroup(), newName).group.getName();
+    return renameGroup(rsrc.toAccountGroup(), newName);
   }
 
-  private GroupDetail renameGroup(AccountGroup group, String newName)
-      throws ResourceConflictException, OrmException, NoSuchGroupException, IOException {
+  private String renameGroup(AccountGroup group, String newName)
+      throws ResourceConflictException, OrmException, IOException {
     AccountGroup.Id groupId = group.getId();
     AccountGroup.NameKey old = group.getNameKey();
     AccountGroup.NameKey key = new AccountGroup.NameKey(newName);
 
     try {
       AccountGroupName id = new AccountGroupName(key, groupId);
-      db.get().accountGroupNames().insert(Collections.singleton(id));
+      db.get().accountGroupNames().insert(ImmutableList.of(id));
     } catch (OrmException e) {
       AccountGroupName other = db.get().accountGroupNames().get(key);
       if (other != null) {
         // If we are using this identity, don't report the exception.
         //
         if (other.getId().equals(groupId)) {
-          return groupDetailFactory.create(groupId).call();
+          return newName;
         }
 
         // Otherwise, someone else has this identity.
@@ -117,12 +111,9 @@
     }
 
     group.setNameKey(key);
-    db.get().accountGroups().update(Collections.singleton(group));
+    db.get().accountGroups().update(ImmutableList.of(group));
 
-    AccountGroupName priorName = db.get().accountGroupNames().get(old);
-    if (priorName != null) {
-      db.get().accountGroupNames().delete(Collections.singleton(priorName));
-    }
+    db.get().accountGroupNames().deleteKeys(ImmutableList.of(old));
 
     groupCache.evict(group);
     groupCache.evictAfterRename(old, key);
@@ -136,6 +127,6 @@
                 newName)
             .start(0, TimeUnit.MILLISECONDS);
 
-    return groupDetailFactory.create(groupId).call();
+    return newName;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/MutableNotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/MutableNotesMigration.java
index e167635..7f4912b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/MutableNotesMigration.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/MutableNotesMigration.java
@@ -72,10 +72,6 @@
     return set(b -> b.setDisableChangeReviewDb(disableChangeReviewDb));
   }
 
-  public MutableNotesMigration setFuseUpdates(boolean fuseUpdates) {
-    return set(b -> b.setFuseUpdates(fuseUpdates));
-  }
-
   public MutableNotesMigration setFailOnLoadForTest(boolean failOnLoadForTest) {
     return set(b -> b.setFailOnLoadForTest(failOnLoadForTest));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
index 9c7029c..e560ec8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
@@ -56,7 +56,6 @@
   public static final String SECTION_NOTE_DB = "noteDb";
 
   private static final String DISABLE_REVIEW_DB = "disableReviewDb";
-  private static final String FUSE_UPDATES = "fuseUpdates";
   private static final String PRIMARY_STORAGE = "primaryStorage";
   private static final String READ = "read";
   private static final String SEQUENCE = "sequence";
@@ -87,7 +86,6 @@
                   SECTION_NOTE_DB, CHANGES.key(), PRIMARY_STORAGE, PrimaryStorage.REVIEW_DB))
           .setDisableChangeReviewDb(
               cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), DISABLE_REVIEW_DB, false))
-          .setFuseUpdates(cfg.getBoolean(SECTION_NOTE_DB, CHANGES.key(), FUSE_UPDATES, false))
           .setFailOnLoadForTest(false) // Only set in tests, can't be set via config.
           .build();
     }
@@ -102,8 +100,6 @@
 
     abstract boolean disableChangeReviewDb();
 
-    abstract boolean fuseUpdates();
-
     abstract boolean failOnLoadForTest();
 
     abstract Builder toBuilder();
@@ -114,7 +110,6 @@
       cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), SEQUENCE, readChangeSequence());
       cfg.setEnum(SECTION_NOTE_DB, CHANGES.key(), PRIMARY_STORAGE, changePrimaryStorage());
       cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), DISABLE_REVIEW_DB, disableChangeReviewDb());
-      cfg.setBoolean(SECTION_NOTE_DB, CHANGES.key(), FUSE_UPDATES, fuseUpdates());
     }
 
     @AutoValue.Builder
@@ -129,8 +124,6 @@
 
       abstract Builder setDisableChangeReviewDb(boolean disableChangeReviewDb);
 
-      abstract Builder setFuseUpdates(boolean fuseUpdates);
-
       abstract Builder setFailOnLoadForTest(boolean failOnLoadForTest);
 
       abstract Snapshot autoBuild();
@@ -206,21 +199,6 @@
   }
 
   /**
-   * Fuse meta ref updates in the same batch as code updates.
-   *
-   * <p>When set, each {@link com.google.gerrit.server.update.BatchUpdate} results in a single
-   * {@link org.eclipse.jgit.lib.BatchRefUpdate} to update both code and meta refs atomically.
-   * Setting this option with a repository backend that does not support atomic multi-ref
-   * transactions ({@link org.eclipse.jgit.lib.RefDatabase#performsAtomicTransactions()}) is a
-   * configuration error, and all updates will fail at runtime.
-   *
-   * <p>Has no effect if {@link #disableChangeReviewDb()} is false.
-   */
-  public final boolean fuseUpdates() {
-    return snapshot.get().fuseUpdates();
-  }
-
-  /**
    * Whether to fail when reading any data from NoteDb.
    *
    * <p>Used in conjunction with {@link #readChanges()} for tests.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigrationState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigrationState.java
index 2469574..c682aed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigrationState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigrationState.java
@@ -31,26 +31,19 @@
  * in the order in which they are defined.
  */
 public enum NotesMigrationState {
-  REVIEW_DB(false, false, false, PrimaryStorage.REVIEW_DB, false, false),
+  REVIEW_DB(false, false, false, PrimaryStorage.REVIEW_DB, false),
 
-  WRITE(false, true, false, PrimaryStorage.REVIEW_DB, false, false),
+  WRITE(false, true, false, PrimaryStorage.REVIEW_DB, false),
 
-  READ_WRITE_NO_SEQUENCE(true, true, false, PrimaryStorage.REVIEW_DB, false, false),
+  READ_WRITE_NO_SEQUENCE(true, true, false, PrimaryStorage.REVIEW_DB, false),
 
-  READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY(
-      true, true, true, PrimaryStorage.REVIEW_DB, false, false),
+  READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY(true, true, true, PrimaryStorage.REVIEW_DB, false),
 
-  READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY(true, true, true, PrimaryStorage.NOTE_DB, false, false),
+  READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY(true, true, true, PrimaryStorage.NOTE_DB, false),
 
-  // TODO(dborowitz): This only exists as a separate state to support testing in different
-  // NoteDbModes. Once FileRepository fuses BatchRefUpdates, we won't have separate fused/unfused
-  // states.
-  NOTE_DB_UNFUSED(true, true, true, PrimaryStorage.NOTE_DB, true, false),
+  NOTE_DB(true, true, true, PrimaryStorage.NOTE_DB, true);
 
-  NOTE_DB(true, true, true, PrimaryStorage.NOTE_DB, true, true);
-
-  // TODO(dborowitz): Replace with NOTE_DB when FileRepository fuses BatchRefUpdates.
-  public static final NotesMigrationState FINAL = NOTE_DB_UNFUSED;
+  public static final NotesMigrationState FINAL = NOTE_DB;
 
   public static Optional<NotesMigrationState> forConfig(Config cfg) {
     return forSnapshot(Snapshot.create(cfg));
@@ -72,8 +65,7 @@
       boolean rawWriteChangesSetting,
       boolean readChangeSequence,
       PrimaryStorage changePrimaryStorage,
-      boolean disableChangeReviewDb,
-      boolean fuseUpdates) {
+      boolean disableChangeReviewDb) {
     this.snapshot =
         Snapshot.builder()
             .setReadChanges(readChanges)
@@ -81,7 +73,6 @@
             .setReadChangeSequence(readChangeSequence)
             .setChangePrimaryStorage(changePrimaryStorage)
             .setDisableChangeReviewDb(disableChangeReviewDb)
-            .setFuseUpdates(fuseUpdates)
             .build();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
index 0a76294..7323c2e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/NoteDbMigrator.java
@@ -19,7 +19,7 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.reviewdb.server.ReviewDbUtil.unwrapDb;
 import static com.google.gerrit.server.notedb.NotesMigration.SECTION_NOTE_DB;
-import static com.google.gerrit.server.notedb.NotesMigrationState.NOTE_DB_UNFUSED;
+import static com.google.gerrit.server.notedb.NotesMigrationState.NOTE_DB;
 import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_NO_SEQUENCE;
 import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY;
 import static com.google.gerrit.server.notedb.NotesMigrationState.READ_WRITE_WITH_SEQUENCE_REVIEW_DB_PRIMARY;
@@ -419,7 +419,7 @@
     }
 
     boolean rebuilt = false;
-    while (state.compareTo(NOTE_DB_UNFUSED) < 0) {
+    while (state.compareTo(NOTE_DB) < 0) {
       if (state.equals(stopAtState)) {
         return;
       }
@@ -458,18 +458,15 @@
           break;
         case READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY:
           // The only way we can get here is if there was a failure on a previous run of
-          // setNoteDbPrimary, since that method moves to NOTE_DB_UNFUSED if it completes
+          // setNoteDbPrimary, since that method moves to NOTE_DB if it completes
           // successfully. Assume that not all changes were converted and re-run the step.
           // migrateToNoteDbPrimary is a relatively fast no-op for already-migrated changes, so this
           // isn't actually repeating work.
           state = setNoteDbPrimary(state);
           break;
-        case NOTE_DB_UNFUSED:
+        case NOTE_DB:
           // Done!
           break;
-        case NOTE_DB:
-          // TODO(dborowitz): Allow this state once FileRepository supports fused updates.
-          // Until then, fallthrough and throw.
         default:
           throw new MigrationException(
               "Migration out of the following state is not supported:\n" + state.toText());
@@ -561,7 +558,7 @@
   }
 
   private NotesMigrationState disableReviewDb(NotesMigrationState prev) throws IOException {
-    return saveState(prev, NOTE_DB_UNFUSED, c -> setAutoMigrate(c, false));
+    return saveState(prev, NOTE_DB, c -> setAutoMigrate(c, false));
   }
 
   private Optional<NotesMigrationState> loadState() throws IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java
index 625bf9e..12028b60 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JsPlugin.java
@@ -40,7 +40,11 @@
     String fileName = getSrcFile().getFileName().toString();
     int firstDash = fileName.indexOf("-");
     if (firstDash > 0) {
-      return fileName.substring(firstDash + 1, fileName.lastIndexOf(".js"));
+      int extension =
+          fileName.endsWith(".js") ? fileName.lastIndexOf(".js") : fileName.lastIndexOf(".html");
+      if (extension > 0) {
+        return fileName.substring(firstDash + 1, extension);
+      }
     }
     return "";
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
index 517c743..0e514d6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.extensions.common.PluginInfo;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.extensions.restapi.Url;
@@ -31,9 +32,11 @@
 import com.google.inject.Inject;
 import java.io.PrintWriter;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.SortedMap;
 import java.util.TreeMap;
+import java.util.regex.Pattern;
 import java.util.stream.Stream;
 import org.kohsuke.args4j.Option;
 
@@ -45,6 +48,9 @@
   private boolean all;
   private int limit;
   private int start;
+  private String matchPrefix;
+  private String matchSubstring;
+  private String matchRegex;
 
   @Deprecated
   @Option(name = "--format", usage = "(deprecated) output format")
@@ -79,6 +85,31 @@
     this.start = start;
   }
 
+  @Option(
+    name = "--prefix",
+    aliases = {"-p"},
+    metaVar = "PREFIX",
+    usage = "match plugin prefix"
+  )
+  public void setMatchPrefix(String matchPrefix) {
+    this.matchPrefix = matchPrefix;
+  }
+
+  @Option(
+    name = "--match",
+    aliases = {"-m"},
+    metaVar = "MATCH",
+    usage = "match plugin substring"
+  )
+  public void setMatchSubstring(String matchSubstring) {
+    this.matchSubstring = matchSubstring;
+  }
+
+  @Option(name = "-r", metaVar = "REGEX", usage = "match plugin regex")
+  public void setMatchRegex(String matchRegex) {
+    this.matchRegex = matchRegex;
+  }
+
   @Inject
   protected ListPlugins(PluginLoader pluginLoader) {
     this.pluginLoader = pluginLoader;
@@ -94,20 +125,33 @@
   }
 
   @Override
-  public Object apply(TopLevelResource resource) {
+  public Object apply(TopLevelResource resource) throws BadRequestException {
     format = OutputFormat.JSON;
     return display(null);
   }
 
-  public SortedMap<String, PluginInfo> apply() {
+  public SortedMap<String, PluginInfo> apply() throws BadRequestException {
     format = OutputFormat.JSON;
     return display(null);
   }
 
-  public SortedMap<String, PluginInfo> display(@Nullable PrintWriter stdout) {
+  public SortedMap<String, PluginInfo> display(@Nullable PrintWriter stdout)
+      throws BadRequestException {
     SortedMap<String, PluginInfo> output = new TreeMap<>();
-    Stream<Plugin> s =
-        Streams.stream(pluginLoader.getPlugins(all)).sorted(comparing(Plugin::getName));
+    Stream<Plugin> s = Streams.stream(pluginLoader.getPlugins(all));
+    if (matchPrefix != null) {
+      checkMatchOptions(matchSubstring == null && matchRegex == null);
+      s = s.filter(p -> p.getName().startsWith(matchPrefix));
+    } else if (matchSubstring != null) {
+      checkMatchOptions(matchPrefix == null && matchRegex == null);
+      String substring = matchSubstring.toLowerCase(Locale.US);
+      s = s.filter(p -> p.getName().toLowerCase(Locale.US).contains(substring));
+    } else if (matchRegex != null) {
+      checkMatchOptions(matchPrefix == null && matchSubstring == null);
+      Pattern pattern = Pattern.compile(matchRegex);
+      s = s.filter(p -> pattern.matcher(p.getName()).matches());
+    }
+    s = s.sorted(comparing(Plugin::getName));
     if (start > 0) {
       s = s.skip(start);
     }
@@ -148,6 +192,12 @@
     return null;
   }
 
+  private void checkMatchOptions(boolean cond) throws BadRequestException {
+    if (!cond) {
+      throw new BadRequestException("specify exactly one of p/m/r");
+    }
+  }
+
   public static PluginInfo toPluginInfo(Plugin p) {
     String id;
     String version;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
index 0c15063..4e2e327 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateBranch.java
@@ -163,6 +163,8 @@
           case REJECTED:
           case REJECTED_CURRENT_BRANCH:
           case RENAMED:
+          case REJECTED_MISSING_OBJECT:
+          case REJECTED_OTHER_REASON:
           default:
             {
               throw new IOException(result.name());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 56151a7..8177e41 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -353,6 +353,8 @@
           case REJECTED:
           case REJECTED_CURRENT_BRANCH:
           case RENAMED:
+          case REJECTED_MISSING_OBJECT:
+          case REJECTED_OTHER_REASON:
           default:
             {
               throw new IOException(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java
index 9b6538c..759f1d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteRef.java
@@ -169,6 +169,8 @@
       case NOT_ATTEMPTED:
       case REJECTED:
       case RENAMED:
+      case REJECTED_MISSING_OBJECT:
+      case REJECTED_OTHER_REASON:
       default:
         log.error("Cannot delete " + ref + ": " + result.name());
         throw new ResourceConflictException("cannot delete: " + result.name());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
index 1a0aff0..e202132 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListBranches.java
@@ -69,7 +69,7 @@
 
   @Option(
     name = "--start",
-    aliases = {"-s"},
+    aliases = {"-S", "-s"},
     metaVar = "CNT",
     usage = "number of branches to skip"
   )
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
index 4d1f808..8d03b6a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListTags.java
@@ -67,7 +67,7 @@
 
   @Option(
     name = "--start",
-    aliases = {"-s"},
+    aliases = {"-S", "-s"},
     metaVar = "CNT",
     usage = "number of tags to skip"
   )
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
index f55d576..65c7315 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
@@ -51,6 +51,12 @@
   /** Invalidate the cached information about the given project. */
   void evict(Project.NameKey p);
 
+  /**
+   * Remove information about the given project from the cache. It will no longer be returned from
+   * {@link #all()}.
+   */
+  void remove(Project p);
+
   /** @return sorted iteration of projects. */
   Iterable<Project.NameKey> all();
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 38ca891..6ee143c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -166,6 +166,21 @@
   }
 
   @Override
+  public void remove(Project p) {
+    listLock.lock();
+    try {
+      SortedSet<Project.NameKey> n = Sets.newTreeSet(list.get(ListKey.ALL));
+      n.remove(p.getNameKey());
+      list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
+    } catch (ExecutionException e) {
+      log.warn("Cannot list available projects", e);
+    } finally {
+      listLock.unlock();
+    }
+    evict(p);
+  }
+
+  @Override
   public void onCreateProject(Project.NameKey newProjectName) {
     listLock.lock();
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
index 90d083b..eeb47df 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
@@ -100,6 +100,8 @@
           case NOT_ATTEMPTED:
           case REJECTED:
           case REJECTED_CURRENT_BRANCH:
+          case REJECTED_MISSING_OBJECT:
+          case REJECTED_OTHER_REASON:
           default:
             throw new IOException("Setting HEAD failed with " + res);
         }
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 fdae8e9..cf88be0 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
@@ -95,8 +95,7 @@
       @Override
       public void configure() {
         factory(ReviewDbBatchUpdate.AssistedFactory.class);
-        factory(FusedNoteDbBatchUpdate.AssistedFactory.class);
-        factory(UnfusedNoteDbBatchUpdate.AssistedFactory.class);
+        factory(NoteDbBatchUpdate.AssistedFactory.class);
       }
     };
   }
@@ -105,29 +104,23 @@
   public static class Factory {
     private final NotesMigration migration;
     private final ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory;
-    private final FusedNoteDbBatchUpdate.AssistedFactory fusedNoteDbBatchUpdateFactory;
-    private final UnfusedNoteDbBatchUpdate.AssistedFactory unfusedNoteDbBatchUpdateFactory;
+    private final NoteDbBatchUpdate.AssistedFactory noteDbBatchUpdateFactory;
 
     // TODO(dborowitz): Make this non-injectable to force all callers to use RetryHelper.
     @Inject
     Factory(
         NotesMigration migration,
         ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory,
-        FusedNoteDbBatchUpdate.AssistedFactory fusedNoteDbBatchUpdateFactory,
-        UnfusedNoteDbBatchUpdate.AssistedFactory unfusedNoteDbBatchUpdateFactory) {
+        NoteDbBatchUpdate.AssistedFactory noteDbBatchUpdateFactory) {
       this.migration = migration;
       this.reviewDbBatchUpdateFactory = reviewDbBatchUpdateFactory;
-      this.fusedNoteDbBatchUpdateFactory = fusedNoteDbBatchUpdateFactory;
-      this.unfusedNoteDbBatchUpdateFactory = unfusedNoteDbBatchUpdateFactory;
+      this.noteDbBatchUpdateFactory = noteDbBatchUpdateFactory;
     }
 
     public BatchUpdate create(
         ReviewDb db, Project.NameKey project, CurrentUser user, Timestamp when) {
       if (migration.disableChangeReviewDb()) {
-        if (migration.fuseUpdates()) {
-          return fusedNoteDbBatchUpdateFactory.create(db, project, user, when);
-        }
-        return unfusedNoteDbBatchUpdateFactory.create(db, project, user, when);
+        return noteDbBatchUpdateFactory.create(db, project, user, when);
       }
       return reviewDbBatchUpdateFactory.create(db, project, user, when);
     }
@@ -147,15 +140,9 @@
       // copy them into an ImmutableList so there is no chance the callee can pollute the input
       // collection.
       if (migration.disableChangeReviewDb()) {
-        if (migration.fuseUpdates()) {
-          ImmutableList<FusedNoteDbBatchUpdate> noteDbUpdates =
-              (ImmutableList) ImmutableList.copyOf(updates);
-          FusedNoteDbBatchUpdate.execute(noteDbUpdates, listener, requestId, dryRun);
-        } else {
-          ImmutableList<UnfusedNoteDbBatchUpdate> noteDbUpdates =
-              (ImmutableList) ImmutableList.copyOf(updates);
-          UnfusedNoteDbBatchUpdate.execute(noteDbUpdates, listener, requestId, dryRun);
-        }
+        ImmutableList<NoteDbBatchUpdate> noteDbUpdates =
+            (ImmutableList) ImmutableList.copyOf(updates);
+        NoteDbBatchUpdate.execute(noteDbUpdates, listener, requestId, dryRun);
       } else {
         ImmutableList<ReviewDbBatchUpdate> reviewDbUpdates =
             (ImmutableList) ImmutableList.copyOf(updates);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
similarity index 95%
rename from gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
index 7db5e43..d23ccf9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/FusedNoteDbBatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/NoteDbBatchUpdate.java
@@ -62,14 +62,14 @@
  * <p>Used when {@code noteDb.changes.disableReviewDb=true}, at which point ReviewDb is not
  * consulted during updates.
  */
-class FusedNoteDbBatchUpdate extends BatchUpdate {
+class NoteDbBatchUpdate extends BatchUpdate {
   interface AssistedFactory {
-    FusedNoteDbBatchUpdate create(
+    NoteDbBatchUpdate create(
         ReviewDb db, Project.NameKey project, CurrentUser user, Timestamp when);
   }
 
   static void execute(
-      ImmutableList<FusedNoteDbBatchUpdate> updates,
+      ImmutableList<NoteDbBatchUpdate> updates,
       BatchUpdateListener listener,
       @Nullable RequestId requestId,
       boolean dryrun)
@@ -88,11 +88,11 @@
       try {
         switch (order) {
           case REPO_BEFORE_DB:
-            for (FusedNoteDbBatchUpdate u : updates) {
+            for (NoteDbBatchUpdate u : updates) {
               u.executeUpdateRepo();
             }
             listener.afterUpdateRepos();
-            for (FusedNoteDbBatchUpdate u : updates) {
+            for (NoteDbBatchUpdate u : updates) {
               handles.add(u.executeChangeOps(dryrun));
             }
             for (ChangesHandle h : handles) {
@@ -113,10 +113,10 @@
             // TODO(dborowitz): This may still result in multiple updates to All-Users, but that's
             // currently not a big deal because multi-change batches generally aren't affecting
             // drafts anyway.
-            for (FusedNoteDbBatchUpdate u : updates) {
+            for (NoteDbBatchUpdate u : updates) {
               handles.add(u.executeChangeOps(dryrun));
             }
-            for (FusedNoteDbBatchUpdate u : updates) {
+            for (NoteDbBatchUpdate u : updates) {
               u.executeUpdateRepo();
             }
             for (ChangesHandle h : handles) {
@@ -151,7 +151,7 @@
               u -> u.gitRefUpdated.fire(u.project, u.batchRefUpdate, u.getAccount().orElse(null)));
 
       if (!dryrun) {
-        for (FusedNoteDbBatchUpdate u : updates) {
+        for (NoteDbBatchUpdate u : updates) {
           u.executePostOps();
         }
       }
@@ -163,7 +163,7 @@
   class ContextImpl implements Context {
     @Override
     public RepoView getRepoView() throws IOException {
-      return FusedNoteDbBatchUpdate.this.getRepoView();
+      return NoteDbBatchUpdate.this.getRepoView();
     }
 
     @Override
@@ -272,7 +272,7 @@
   private final ReviewDb db;
 
   @Inject
-  FusedNoteDbBatchUpdate(
+  NoteDbBatchUpdate(
       GitRepositoryManager repoManager,
       @GerritPersonIdent PersonIdent serverIdent,
       ChangeNotes.Factory changeNotesFactory,
@@ -354,7 +354,7 @@
     }
 
     void execute() throws OrmException, IOException {
-      FusedNoteDbBatchUpdate.this.batchRefUpdate = manager.execute(dryrun);
+      NoteDbBatchUpdate.this.batchRefUpdate = manager.execute(dryrun);
     }
 
     @SuppressWarnings("deprecation")
@@ -390,8 +390,7 @@
     Repository repo = repoView.getRepository();
     checkState(
         repo.getRefDatabase().performsAtomicTransactions(),
-        "cannot use noteDb.changes.fuseUpdates=true with a repository that does not support atomic"
-            + " batch ref updates: %s",
+        "cannot use NoteDb with a repository that does not support atomic batch ref updates: %s",
         repo);
 
     ChangesHandle handle =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java
index 25a4e3c..7a1de31 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/update/RetryHelper.java
@@ -51,15 +51,10 @@
       @GerritServerConfig Config cfg,
       NotesMigration migration,
       ReviewDbBatchUpdate.AssistedFactory reviewDbBatchUpdateFactory,
-      FusedNoteDbBatchUpdate.AssistedFactory fusedNoteDbBatchUpdateFactory,
-      UnfusedNoteDbBatchUpdate.AssistedFactory unfusedNoteDbBatchUpdateFactory) {
+      NoteDbBatchUpdate.AssistedFactory noteDbBatchUpdateFactory) {
     this.migration = migration;
     this.updateFactory =
-        new BatchUpdate.Factory(
-            migration,
-            reviewDbBatchUpdateFactory,
-            fusedNoteDbBatchUpdateFactory,
-            unfusedNoteDbBatchUpdateFactory);
+        new BatchUpdate.Factory(migration, reviewDbBatchUpdateFactory, noteDbBatchUpdateFactory);
     this.stopStrategy =
         StopStrategies.stopAfterDelay(
             cfg.getTimeUnit("noteDb", null, "retryTimeout", SECONDS.toMillis(5), MILLISECONDS),
@@ -80,7 +75,7 @@
       throws RestApiException, UpdateException {
     try {
       RetryerBuilder<T> builder = RetryerBuilder.newBuilder();
-      if (migration.disableChangeReviewDb() && migration.fuseUpdates()) {
+      if (migration.disableChangeReviewDb()) {
         builder
             .withStopStrategy(stopStrategy)
             .withWaitStrategy(waitStrategy)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java
deleted file mode 100644
index ce96c0e..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/update/UnfusedNoteDbBatchUpdate.java
+++ /dev/null
@@ -1,459 +0,0 @@
-// 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.server.update;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-import static java.util.Comparator.comparing;
-import static java.util.stream.Collectors.toList;
-
-import com.google.common.base.Throwables;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Maps;
-import com.google.gerrit.common.Nullable;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.index.change.ChangeIndexer;
-import com.google.gerrit.server.notedb.ChangeNotes;
-import com.google.gerrit.server.notedb.ChangeUpdate;
-import com.google.gerrit.server.notedb.NoteDbUpdateManager;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.util.RequestId;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-import java.io.IOException;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.TimeZone;
-import java.util.TreeMap;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.transport.ReceiveCommand;
-
-/**
- * {@link BatchUpdate} implementation that only supports NoteDb.
- *
- * <p>Used when {@code noteDb.changes.disableReviewDb=true}, at which point ReviewDb is not
- * consulted during updates.
- */
-class UnfusedNoteDbBatchUpdate extends BatchUpdate {
-  interface AssistedFactory {
-    UnfusedNoteDbBatchUpdate create(
-        ReviewDb db, Project.NameKey project, CurrentUser user, Timestamp when);
-  }
-
-  static void execute(
-      ImmutableList<UnfusedNoteDbBatchUpdate> updates,
-      BatchUpdateListener listener,
-      @Nullable RequestId requestId,
-      boolean dryrun)
-      throws UpdateException, RestApiException {
-    if (updates.isEmpty()) {
-      return;
-    }
-    setRequestIds(updates, requestId);
-
-    try {
-      Order order = getOrder(updates, listener);
-      // TODO(dborowitz): Fuse implementations to use a single BatchRefUpdate between phases. Note
-      // that we may still need to respect the order, since op implementations may make assumptions
-      // about the order in which their methods are called.
-      switch (order) {
-        case REPO_BEFORE_DB:
-          for (UnfusedNoteDbBatchUpdate u : updates) {
-            u.executeUpdateRepo();
-          }
-          listener.afterUpdateRepos();
-          for (UnfusedNoteDbBatchUpdate u : updates) {
-            u.executeRefUpdates(dryrun);
-          }
-          listener.afterUpdateRefs();
-          for (UnfusedNoteDbBatchUpdate u : updates) {
-            u.reindexChanges(u.executeChangeOps(dryrun), dryrun);
-          }
-          listener.afterUpdateChanges();
-          break;
-        case DB_BEFORE_REPO:
-          for (UnfusedNoteDbBatchUpdate u : updates) {
-            u.reindexChanges(u.executeChangeOps(dryrun), dryrun);
-          }
-          for (UnfusedNoteDbBatchUpdate u : updates) {
-            u.executeUpdateRepo();
-          }
-          for (UnfusedNoteDbBatchUpdate u : updates) {
-            u.executeRefUpdates(dryrun);
-          }
-          break;
-        default:
-          throw new IllegalStateException("invalid execution order: " + order);
-      }
-
-      ChangeIndexer.allAsList(
-              updates.stream().flatMap(u -> u.indexFutures.stream()).collect(toList()))
-          .get();
-
-      // Fire ref update events only after all mutations are finished, since callers may assume a
-      // patch set ref being created means the change was created, or a branch advancing meaning
-      // some changes were closed.
-      updates
-          .stream()
-          .filter(u -> u.batchRefUpdate != null)
-          .forEach(
-              u -> u.gitRefUpdated.fire(u.project, u.batchRefUpdate, u.getAccount().orElse(null)));
-
-      if (!dryrun) {
-        for (UnfusedNoteDbBatchUpdate u : updates) {
-          u.executePostOps();
-        }
-      }
-    } catch (Exception e) {
-      wrapAndThrowException(e);
-    }
-  }
-
-  class ContextImpl implements Context {
-    @Override
-    public RepoView getRepoView() throws IOException {
-      return UnfusedNoteDbBatchUpdate.this.getRepoView();
-    }
-
-    @Override
-    public RevWalk getRevWalk() throws IOException {
-      return getRepoView().getRevWalk();
-    }
-
-    @Override
-    public Project.NameKey getProject() {
-      return project;
-    }
-
-    @Override
-    public Timestamp getWhen() {
-      return when;
-    }
-
-    @Override
-    public TimeZone getTimeZone() {
-      return tz;
-    }
-
-    @Override
-    public ReviewDb getDb() {
-      return db;
-    }
-
-    @Override
-    public CurrentUser getUser() {
-      return user;
-    }
-
-    @Override
-    public Order getOrder() {
-      return order;
-    }
-  }
-
-  private class RepoContextImpl extends ContextImpl implements RepoContext {
-    @Override
-    public ObjectInserter getInserter() throws IOException {
-      return getRepoView().getInserterWrapper();
-    }
-
-    @Override
-    public void addRefUpdate(ReceiveCommand cmd) throws IOException {
-      getRepoView().getCommands().add(cmd);
-    }
-  }
-
-  private class ChangeContextImpl extends ContextImpl implements ChangeContext {
-    private final ChangeControl ctl;
-    private final Map<PatchSet.Id, ChangeUpdate> updates;
-
-    private boolean deleted;
-
-    protected ChangeContextImpl(ChangeControl ctl) {
-      this.ctl = checkNotNull(ctl);
-      updates = new TreeMap<>(comparing(PatchSet.Id::get));
-    }
-
-    @Override
-    public ChangeUpdate getUpdate(PatchSet.Id psId) {
-      ChangeUpdate u = updates.get(psId);
-      if (u == null) {
-        u = changeUpdateFactory.create(ctl, when);
-        if (newChanges.containsKey(ctl.getId())) {
-          u.setAllowWriteToNewRef(true);
-        }
-        u.setPatchSetId(psId);
-        updates.put(psId, u);
-      }
-      return u;
-    }
-
-    @Override
-    public ChangeControl getControl() {
-      return ctl;
-    }
-
-    @Override
-    public void dontBumpLastUpdatedOn() {
-      // Do nothing; NoteDb effectively updates timestamp if and only if a commit was written to the
-      // change meta ref.
-    }
-
-    @Override
-    public void deleteChange() {
-      deleted = true;
-    }
-  }
-
-  /** Per-change result status from {@link #executeChangeOps}. */
-  private enum ChangeResult {
-    SKIPPED,
-    UPSERTED,
-    DELETED;
-  }
-
-  private final ChangeNotes.Factory changeNotesFactory;
-  private final ChangeControl.GenericFactory changeControlFactory;
-  private final ChangeUpdate.Factory changeUpdateFactory;
-  private final NoteDbUpdateManager.Factory updateManagerFactory;
-  private final ChangeIndexer indexer;
-  private final GitReferenceUpdated gitRefUpdated;
-  private final ReviewDb db;
-
-  @SuppressWarnings("deprecation")
-  private List<com.google.common.util.concurrent.CheckedFuture<?, IOException>> indexFutures;
-
-  @Inject
-  UnfusedNoteDbBatchUpdate(
-      GitRepositoryManager repoManager,
-      @GerritPersonIdent PersonIdent serverIdent,
-      ChangeNotes.Factory changeNotesFactory,
-      ChangeControl.GenericFactory changeControlFactory,
-      ChangeUpdate.Factory changeUpdateFactory,
-      NoteDbUpdateManager.Factory updateManagerFactory,
-      ChangeIndexer indexer,
-      GitReferenceUpdated gitRefUpdated,
-      @Assisted ReviewDb db,
-      @Assisted Project.NameKey project,
-      @Assisted CurrentUser user,
-      @Assisted Timestamp when) {
-    super(repoManager, serverIdent, project, user, when);
-    this.changeNotesFactory = changeNotesFactory;
-    this.changeControlFactory = changeControlFactory;
-    this.changeUpdateFactory = changeUpdateFactory;
-    this.updateManagerFactory = updateManagerFactory;
-    this.indexer = indexer;
-    this.gitRefUpdated = gitRefUpdated;
-    this.db = db;
-    this.indexFutures = new ArrayList<>();
-  }
-
-  @Override
-  public void execute(BatchUpdateListener listener) throws UpdateException, RestApiException {
-    execute(ImmutableList.of(this), listener, requestId, false);
-  }
-
-  @Override
-  protected Context newContext() {
-    return new ContextImpl();
-  }
-
-  private void executeUpdateRepo() throws UpdateException, RestApiException {
-    try {
-      logDebug("Executing updateRepo on {} ops", ops.size());
-      RepoContextImpl ctx = new RepoContextImpl();
-      for (BatchUpdateOp op : ops.values()) {
-        op.updateRepo(ctx);
-      }
-
-      logDebug("Executing updateRepo on {} RepoOnlyOps", repoOnlyOps.size());
-      for (RepoOnlyOp op : repoOnlyOps) {
-        op.updateRepo(ctx);
-      }
-
-      if (onSubmitValidators != null && !getRefUpdates().isEmpty()) {
-        // Validation of refs has to take place here and not at the beginning of executeRefUpdates.
-        // Otherwise, failing validation in a second BatchUpdate object will happen *after* the
-        // first update's executeRefUpdates has finished, hence after first repo's refs have been
-        // updated, which is too late.
-        onSubmitValidators.validate(
-            project, ctx.getRevWalk().getObjectReader(), repoView.getCommands());
-      }
-
-      // TODO(dborowitz): Don't flush when fusing phases.
-      if (repoView != null) {
-        logDebug("Flushing inserter");
-        repoView.getInserter().flush();
-      } else {
-        logDebug("No objects to flush");
-      }
-    } catch (Exception e) {
-      Throwables.throwIfInstanceOf(e, RestApiException.class);
-      throw new UpdateException(e);
-    }
-  }
-
-  // TODO(dborowitz): Don't execute non-change ref updates separately when fusing phases.
-  private void executeRefUpdates(boolean dryrun) throws IOException, RestApiException {
-    if (getRefUpdates().isEmpty()) {
-      logDebug("No ref updates to execute");
-      return;
-    }
-    // May not be opened if the caller added ref updates but no new objects.
-    initRepository();
-    batchRefUpdate = repoView.getRepository().getRefDatabase().newBatchUpdate();
-    batchRefUpdate.setPushCertificate(pushCert);
-    batchRefUpdate.setRefLogMessage(refLogMessage, true);
-    batchRefUpdate.setAllowNonFastForwards(true);
-    repoView.getCommands().addTo(batchRefUpdate);
-    logDebug("Executing batch of {} ref updates", batchRefUpdate.getCommands().size());
-    if (dryrun) {
-      return;
-    }
-
-    // Force BatchRefUpdate to read newly referenced objects using a new RevWalk, rather than one
-    // that might have access to unflushed objects.
-    try (RevWalk updateRw = new RevWalk(repoView.getRepository())) {
-      batchRefUpdate.execute(updateRw, NullProgressMonitor.INSTANCE);
-    }
-    boolean ok = true;
-    for (ReceiveCommand cmd : batchRefUpdate.getCommands()) {
-      if (cmd.getResult() != ReceiveCommand.Result.OK) {
-        ok = false;
-        break;
-      }
-    }
-    if (!ok) {
-      throw new RestApiException("BatchRefUpdate failed: " + batchRefUpdate);
-    }
-  }
-
-  private Map<Change.Id, ChangeResult> executeChangeOps(boolean dryrun) throws Exception {
-    logDebug("Executing change ops");
-    Map<Change.Id, ChangeResult> result =
-        Maps.newLinkedHashMapWithExpectedSize(ops.keySet().size());
-    initRepository();
-    Repository repo = repoView.getRepository();
-    // TODO(dborowitz): Teach NoteDbUpdateManager to allow reusing the same inserter and batch ref
-    // update as in executeUpdateRepo.
-    try (ObjectInserter ins = repo.newObjectInserter();
-        ObjectReader reader = ins.newReader();
-        RevWalk rw = new RevWalk(reader);
-        NoteDbUpdateManager updateManager =
-            updateManagerFactory
-                .create(project)
-                .setChangeRepo(repo, rw, ins, new ChainedReceiveCommands(repo))) {
-      if (user.isIdentifiedUser()) {
-        updateManager.setRefLogIdent(user.asIdentifiedUser().newRefLogIdent(when, tz));
-      }
-      for (Map.Entry<Change.Id, Collection<BatchUpdateOp>> e : ops.asMap().entrySet()) {
-        Change.Id id = e.getKey();
-        ChangeContextImpl ctx = newChangeContext(id);
-        boolean dirty = false;
-        logDebug("Applying {} ops for change {}", e.getValue().size(), id);
-        for (BatchUpdateOp op : e.getValue()) {
-          dirty |= op.updateChange(ctx);
-        }
-        if (!dirty) {
-          logDebug("No ops reported dirty, short-circuiting");
-          result.put(id, ChangeResult.SKIPPED);
-          continue;
-        }
-        for (ChangeUpdate u : ctx.updates.values()) {
-          updateManager.add(u);
-        }
-        if (ctx.deleted) {
-          logDebug("Change {} was deleted", id);
-          updateManager.deleteChange(id);
-          result.put(id, ChangeResult.DELETED);
-        } else {
-          result.put(id, ChangeResult.UPSERTED);
-        }
-      }
-
-      if (!dryrun) {
-        logDebug("Executing NoteDb updates");
-        updateManager.execute();
-      }
-    }
-    return result;
-  }
-
-  private ChangeContextImpl newChangeContext(Change.Id id) throws OrmException {
-    logDebug("Opening change {} for update", id);
-    Change c = newChanges.get(id);
-    boolean isNew = c != null;
-    if (!isNew) {
-      // Pass a synthetic change into ChangeNotes.Factory, which will take care of checking for
-      // existence and populating columns from the parsed notes state.
-      // TODO(dborowitz): This dance made more sense when using Reviewdb; consider a nicer way.
-      c = ChangeNotes.Factory.newNoteDbOnlyChange(project, id);
-    } else {
-      logDebug("Change {} is new", id);
-    }
-    ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c, !isNew);
-    ChangeControl ctl = changeControlFactory.controlFor(notes, user);
-    return new ChangeContextImpl(ctl);
-  }
-
-  private void reindexChanges(Map<Change.Id, ChangeResult> updateResults, boolean dryrun) {
-    if (dryrun) {
-      return;
-    }
-    logDebug("Reindexing {} changes", updateResults.size());
-    for (Map.Entry<Change.Id, ChangeResult> e : updateResults.entrySet()) {
-      Change.Id id = e.getKey();
-      switch (e.getValue()) {
-        case UPSERTED:
-          indexFutures.add(indexer.indexAsync(project, id));
-          break;
-        case DELETED:
-          indexFutures.add(indexer.deleteAsync(id));
-          break;
-        case SKIPPED:
-          break;
-        default:
-          throw new IllegalStateException("unexpected result: " + e.getValue());
-      }
-    }
-  }
-
-  private void executePostOps() throws Exception {
-    ContextImpl ctx = new ContextImpl();
-    for (BatchUpdateOp op : ops.values()) {
-      op.postUpdate(ctx);
-    }
-
-    for (RepoOnlyOp op : repoOnlyOps) {
-      op.postUpdate(ctx);
-    }
-  }
-}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index 29f17b4..9b9cfff 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -239,6 +239,9 @@
           public void evict(Project p) {}
 
           @Override
+          public void remove(Project p) {}
+
+          @Override
           public Iterable<Project.NameKey> all() {
             return Collections.emptySet();
           }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
index 0e7201a..e0c51b7 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.RepositoryCaseMismatchException;
-import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.inject.Inject;
 import java.util.HashMap;
 import java.util.Map;
@@ -32,7 +31,7 @@
 /** Repository manager that uses in-memory repositories. */
 public class InMemoryRepositoryManager implements GitRepositoryManager {
   public static InMemoryRepository newRepository(Project.NameKey name) {
-    return new Repo(NoteDbMode.newNotesMigrationFromEnv(), name);
+    return new Repo(name);
   }
 
   public static class Description extends DfsRepositoryDescription {
@@ -51,15 +50,9 @@
   public static class Repo extends InMemoryRepository {
     private String description;
 
-    private Repo(NotesMigration notesMigration, Project.NameKey name) {
+    private Repo(Project.NameKey name) {
       super(new Description(name));
-      // Normally, mimic the behavior of JGit FileRepository, the standard Gerrit repository
-      // backend, and don't support atomic ref updates. The exception is when we're testing with
-      // fused ref updates, which requires atomic ref updates to function.
-      //
-      // TODO(dborowitz): Change to match the behavior of JGit FileRepository after fixing
-      // https://bugs.eclipse.org/bugs/show_bug.cgi?id=515678
-      setPerformsAtomicTransactions(notesMigration.fuseUpdates());
+      setPerformsAtomicTransactions(true);
     }
 
     @Override
@@ -78,16 +71,10 @@
     }
   }
 
-  private final NotesMigration notesMigration;
   private final Map<String, Repo> repos;
 
-  public InMemoryRepositoryManager() {
-    this(NoteDbMode.newNotesMigrationFromEnv());
-  }
-
   @Inject
-  InMemoryRepositoryManager(NotesMigration notesMigration) {
-    this.notesMigration = notesMigration;
+  public InMemoryRepositoryManager() {
     this.repos = new HashMap<>();
   }
 
@@ -106,7 +93,7 @@
         throw new RepositoryCaseMismatchException(name);
       }
     } catch (RepositoryNotFoundException e) {
-      repo = new Repo(notesMigration, name);
+      repo = new Repo(name);
       repos.put(normalize(name), repo);
     }
     return repo;
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java
index 7c19191..b51a011 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java
@@ -35,11 +35,15 @@
   PRIMARY(NotesMigrationState.READ_WRITE_WITH_SEQUENCE_NOTE_DB_PRIMARY),
 
   /** All change tables are entirely disabled. */
-  DISABLE_CHANGE_REVIEW_DB(NotesMigrationState.NOTE_DB_UNFUSED),
+  DISABLE_CHANGE_REVIEW_DB(NotesMigrationState.NOTE_DB),
 
   /** All change tables are entirely disabled, and code/meta ref updates are fused. */
   FUSED(NotesMigrationState.NOTE_DB),
 
+  // TODO(dborowitz): Change CI to use this, then remove FUSED and DISABLE_CHANGE_REVIEW_DB.
+  /** All change tables are entirely disabled, and code/meta ref updates are fused. */
+  ON(NotesMigrationState.NOTE_DB),
+
   /**
    * Run tests with NoteDb disabled, then convert ReviewDb to NoteDb and check that the results
    * match.
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
index 49b468f..6ec3a28 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
@@ -52,7 +51,7 @@
       PutName.Input input = new PutName.Input();
       input.name = newGroupName;
       putName.apply(rsrc, input);
-    } catch (RestApiException | OrmException | IOException | NoSuchGroupException e) {
+    } catch (RestApiException | OrmException | IOException e) {
       throw die(e);
     }
   }
diff --git a/lib/jgit/jgit.bzl b/lib/jgit/jgit.bzl
index fd10fbb..b6f8d99 100644
--- a/lib/jgit/jgit.bzl
+++ b/lib/jgit/jgit.bzl
@@ -1,12 +1,12 @@
 load("//tools/bzl:maven_jar.bzl", "GERRIT", "MAVEN_LOCAL", "MAVEN_CENTRAL", "maven_jar")
 
-_JGIT_VERS = "4.8.0.201706111038-r"
+_JGIT_VERS = "4.8.0.201706111038-r.71-g45da0fc6f"
 
 _DOC_VERS = _JGIT_VERS # Set to _JGIT_VERS unless using a snapshot
 
 JGIT_DOC_URL = "http://download.eclipse.org/jgit/site/" + _DOC_VERS + "/apidocs"
 
-_JGIT_REPO = MAVEN_CENTRAL # Leave here even if set to MAVEN_CENTRAL.
+_JGIT_REPO = GERRIT # Leave here even if set to MAVEN_CENTRAL.
 
 # set this to use a local version.
 # "/home/<user>/projects/jgit"
@@ -26,28 +26,28 @@
         name = "jgit_lib",
         artifact = "org.eclipse.jgit:org.eclipse.jgit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "f0978a9e868accf9a405d9387bec091a99d87633",
-        src_sha1 = "93cefbf1d73f1179b40419a3898c53a64e52aa93",
+        sha1 = "7248b0a7d7f76dd4f7e55ed081b981cf4d8aa26e",
+        src_sha1 = "6ed203c95decc3f795f44ca17149e7554b92212d",
         unsign = True,
     )
     maven_jar(
         name = "jgit_servlet",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.http.server:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "3c099afdc063bad438a3b87eea643e9722a07de8",
+        sha1 = "f21fc0c651cc9475db92061432d919ba28b7a7ad",
         unsign = True,
     )
     maven_jar(
         name = "jgit_archive",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.archive:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "1350a5cf1fad91dd33b66f9fb804dc8e68270890",
+        sha1 = "0f179321f527840dfc8ca79894eba2f6b255dbab",
     )
     maven_jar(
         name = "jgit_junit",
         artifact = "org.eclipse.jgit:org.eclipse.jgit.junit:" + _JGIT_VERS,
         repository = _JGIT_REPO,
-        sha1 = "4f45f8f6714df649dbad8c1b1baf68b9510b5047",
+        sha1 = "7e5225064cf14115bddaae9448246c83c89f78ad",
         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 99efede..706d499 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
@@ -16,7 +16,7 @@
 -->
 
 <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
-<title>keyboard-shortcut-behavior</title>
+<title>base-url-behavior</title>
 
 <script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
 <script src="../../bower_components/web-component-tester/browser.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
index 4178a0c..3d26b55 100644
--- a/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
+++ b/polygerrit-ui/app/elements/admin/gr-admin-view/gr-admin-view.html
@@ -87,7 +87,7 @@
     </template>
     <template is="dom-if" if="[[_showPluginList]]" restamp="true">
       <main class="table">
-        <gr-plugin-list class="table"></gr-plugin-list>
+        <gr-plugin-list class="table" params="[[params]]"></gr-plugin-list>
       </main>
     </template>
     <template is="dom-if" if="[[_showProjectDetailList]]" restamp="true">
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html
index 788df49..7d12e96 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.html
@@ -18,33 +18,42 @@
 <link rel="import" href="../../../behaviors/gr-list-view-behavior/gr-list-view-behavior.html">
 <link rel="import" href="../../../styles/gr-table-styles.html">
 <link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-list-view/gr-list-view.html">
 <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
 
 <dom-module id="gr-plugin-list">
   <template>
     <style include="shared-styles"></style>
     <style include="gr-table-styles"></style>
-    <table id="list" class="genericList">
-      <tr class="headerRow">
-        <th class="name topHeader">Plugin Name</th>
-        <th class="version topHeader">Version</th>
-        <th class="status topHeader">Status</th>
-      </tr>
-      <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
-        <td>Loading...</td>
-      </tr>
-      <tbody class$="[[computeLoadingClass(_loading)]]">
-        <template is="dom-repeat" items="[[_plugins]]">
-          <tr class="table">
-            <td class="name">
-              <a href$="[[_computePluginUrl(item.index_url)]]">[[item.id]]</a>
-            </td>
-            <td class="version">[[item.version]]</td>
-            <td class="status">[[_status(item)]]</td>
-          </tr>
-        </template>
-      </tbody>
-    </table>
+    <gr-list-view
+        filter="[[_filter]]"
+        items-per-page="[[_pluginsPerPage]]"
+        items="[[_plugins]]"
+        loading="[[_loading]]"
+        offset="[[_offset]]"
+        path="[[_path]]">
+      <table id="list" class="genericList">
+        <tr class="headerRow">
+          <th class="name topHeader">Plugin Name</th>
+          <th class="version topHeader">Version</th>
+          <th class="status topHeader">Status</th>
+        </tr>
+        <tr id="loading" class$="loadingMsg [[computeLoadingClass(_loading)]]">
+          <td>Loading...</td>
+        </tr>
+        <tbody class$="[[computeLoadingClass(_loading)]]">
+          <template is="dom-repeat" items="[[_shownPlugins]]">
+            <tr class="table">
+              <td class="name">
+                <a href$="[[_computePluginUrl(item.index_url)]]">[[item.id]]</a>
+              </td>
+              <td class="version">[[item.version]]</td>
+              <td class="status">[[_status(item)]]</td>
+            </tr>
+          </template>
+        </tbody>
+      </table>
+    </gr-list-view>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
   </template>
   <script src="gr-plugin-list.js"></script>
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
index b4fa9ad..a18a862 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list.js
@@ -18,23 +18,61 @@
     is: 'gr-plugin-list',
 
     properties: {
+      /**
+       * URL params passed from the router.
+       */
+      params: {
+        type: Object,
+        observer: '_paramsChanged',
+      },
+      /**
+       * Offset of currently visible query results.
+       */
+      _offset: Number,
+      _path: {
+        type: String,
+        readOnly: true,
+        value: '/admin/plugins',
+      },
       _plugins: Array,
+      /**
+       * Because  we request one more than the pluginsPerPage, _shownPlugins
+       * maybe one less than _plugins.
+       * */
+      _shownPlugins: {
+        type: Array,
+        computed: 'computeShownItems(_plugins)',
+      },
+      _pluginsPerPage: {
+        type: Number,
+        value: 25,
+      },
       _loading: {
         type: Boolean,
         value: true,
       },
+      _filter: String,
     },
 
     behaviors: [
       Gerrit.ListViewBehavior,
     ],
 
-    ready() {
-      this._getPlugins();
+    attached() {
+      this.fire('title-change', {title: 'Plugin List'});
     },
 
-    _getPlugins() {
-      return this.$.restAPI.getPlugins()
+    _paramsChanged(params) {
+      this._loading = true;
+      this._filter = this.getFilterValue(params);
+      this._offset = this.getOffsetValue(params);
+
+      return this._getPlugins(this._filter, this._pluginsPerPage,
+          this._offset);
+    },
+
+    _getPlugins(filter, pluginsPerPage, offset) {
+      return this.$.restAPI.getPlugins(filter, pluginsPerPage, offset)
           .then(plugins => {
             if (!plugins) {
               this._plugins = [];
diff --git a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
index 8cff477..52f1a22 100644
--- a/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
+++ b/polygerrit-ui/app/elements/admin/gr-plugin-list/gr-plugin-list_test.html
@@ -33,7 +33,7 @@
 </test-fixture>
 
 <script>
-  let counter = 0;
+  let counter;
   const pluginGenerator = () => {
     return {
       id: `test${++counter}`,
@@ -47,26 +47,93 @@
     let element;
     let plugins;
     let sandbox;
+    let value;
 
     setup(() => {
       sandbox = sinon.sandbox.create();
-      plugins = _.times(26, pluginGenerator);
-      stub('gr-rest-api-interface', {
-        getPlugins() { return Promise.resolve(plugins); },
-      });
       element = fixture('basic');
+      counter = 0;
     });
 
     teardown(() => {
       sandbox.restore();
     });
 
-    test('_plugins item is formatted correctly', () => {
-      return element._getPlugins().then(() => {
-        assert.equal(element._plugins[2].id, 'test3');
-        assert.equal(element._plugins[2].index_url, 'plugins/test3/');
-        assert.equal(element._plugins[2].version, '3.0-SNAPSHOT');
-        assert.equal(element._plugins[2].disabled, false);
+    suite('list with plugins', () => {
+      setup(done => {
+        plugins = _.times(26, pluginGenerator);
+
+        stub('gr-rest-api-interface', {
+          getPlugins(num, offset) {
+            return Promise.resolve(plugins);
+          },
+        });
+
+        element._paramsChanged(value).then(() => { flush(done); });
+      });
+
+      test('plugin in the list is formatted correctly', done => {
+        flush(() => {
+          assert.equal(element._plugins[2].id, 'test3');
+          assert.equal(element._plugins[2].index_url, 'plugins/test3/');
+          assert.equal(element._plugins[2].version, '3.0-SNAPSHOT');
+          assert.equal(element._plugins[2].disabled, false);
+          done();
+        });
+      });
+
+      test('_shownPlugins', () => {
+        assert.equal(element._shownPlugins.length, 25);
+      });
+    });
+
+    suite('list with less then 26 plugins', () => {
+      setup(done => {
+        plugins = _.times(25, pluginGenerator);
+
+        stub('gr-rest-api-interface', {
+          getPlugins(num, offset) {
+            return Promise.resolve(plugins);
+          },
+        });
+
+        element._paramsChanged(value).then(() => { flush(done); });
+      });
+
+      test('_shownPlugins', () => {
+        assert.equal(element._shownPlugins.length, 25);
+      });
+    });
+
+    suite('filter', () => {
+      test('_paramsChanged', done => {
+        sandbox.stub(element.$.restAPI, 'getPlugins', () => {
+          return Promise.resolve(plugins);
+        });
+        const value = {
+          filter: 'test',
+          offset: 25,
+        };
+        element._paramsChanged(value).then(() => {
+          assert.isTrue(element.$.restAPI.getPlugins.lastCall
+              .calledWithExactly('test', 25, 25));
+          done();
+        });
+      });
+    });
+
+    suite('loading', () => {
+      test('correct contents are displayed', () => {
+        assert.isTrue(element._loading);
+        assert.equal(element.computeLoadingClass(element._loading), 'loading');
+        assert.equal(getComputedStyle(element.$.loading).display, 'block');
+
+        element._loading = false;
+        element._plugins = _.times(25, pluginGenerator);
+
+        flushAsynchronousOperations();
+        assert.equal(element.computeLoadingClass(element._loading), '');
+        assert.equal(getComputedStyle(element.$.loading).display, 'none');
       });
     });
   });
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index e7b15af..af5a3b0 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -103,6 +103,7 @@
       'o enter': '_handleEnterKey',
       'p [': '_handlePKey',
       'shift+r': '_handleRKey',
+      's': '_handleSKey',
     },
 
     attached() {
@@ -163,7 +164,9 @@
     },
 
     _computeLabelShortcut(labelName) {
-      return labelName.replace(/[a-z-]/g, '');
+      return labelName.split('-').reduce((a, i) => {
+        return a + i[0].toUpperCase();
+      }, '');
     },
 
     _changesChanged(changes) {
@@ -260,6 +263,27 @@
       window.location.reload();
     },
 
+    _handleSKey(e) {
+      if (this.shouldSuppressKeyboardShortcut(e) ||
+          this.modifierPressed(e)) { return; }
+
+      e.preventDefault();
+      this._toggleStarForIndex(this.selectedIndex);
+    },
+
+    _toggleStarForIndex(index) {
+      const changeEls = this._getListItems();
+      if (index >= changeEls.length || !changeEls[index]) {
+        return;
+      }
+
+      const changeEl = changeEls[index];
+      const change = changeEl.change;
+      const newVal = !change.starred;
+      changeEl.set('change.starred', newVal);
+      this.$.restAPI.saveChangeStarred(change._number, newVal);
+    },
+
     _changeURLForIndex(index) {
       const changeEls = this._getListItems();
       if (index < changeEls.length && changeEls[index]) {
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
index 378cd7b..a570bd5 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list_test.html
@@ -115,6 +115,8 @@
       assert.equal(element._computeLabelShortcut('Code-Review'), 'CR');
       assert.equal(element._computeLabelShortcut('Verified'), 'V');
       assert.equal(element._computeLabelShortcut('Library-Compliance'), 'LC');
+      assert.equal(element._computeLabelShortcut('PolyGerrit-Review'), 'PR');
+      assert.equal(element._computeLabelShortcut('polygerrit-review'), 'PR');
       assert.equal(element._computeLabelShortcut(
           'Some-Special-Label-7'), 'SSL7');
     });
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
index d751cff..18ba163 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.html
@@ -29,6 +29,7 @@
 
 <link rel="import" href="../gr-confirm-abandon-dialog/gr-confirm-abandon-dialog.html">
 <link rel="import" href="../gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog.html">
+<link rel="import" href="../gr-confirm-move-dialog/gr-confirm-move-dialog.html">
 <link rel="import" href="../gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.html">
 <link rel="import" href="../gr-confirm-revert-dialog/gr-confirm-revert-dialog.html">
 <link rel="import" href="../../../styles/shared-styles.html">
@@ -124,6 +125,12 @@
           on-cancel="_handleConfirmDialogCancel"
           project="[[change.project]]"
           hidden></gr-confirm-cherrypick-dialog>
+      <gr-confirm-move-dialog id="confirmMove"
+          class="confirmDialog"
+          on-confirm="_handleMoveConfirm"
+          on-cancel="_handleConfirmDialogCancel"
+          project="[[change.project]]"
+          hidden></gr-confirm-move-dialog>
       <gr-confirm-revert-dialog id="confirmRevertDialog"
           class="confirmDialog"
           on-confirm="_handleRevertDialogConfirm"
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 3d5654e..329a343 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -51,6 +51,7 @@
     ABANDON: 'abandon',
     DELETE: '/',
     IGNORE: 'ignore',
+    MOVE: 'move',
     MUTE: 'mute',
     PRIVATE: 'private',
     PRIVATE_DELETE: 'private.delete',
@@ -75,6 +76,7 @@
     abandon: 'Abandoning...',
     cherrypick: 'Cherry-Picking...',
     delete: 'Deleting...',
+    move: 'Moving..',
     publish: 'Publishing...',
     rebase: 'Rebasing...',
     restore: 'Restoring...',
@@ -217,6 +219,10 @@
               key: RevisionActions.CHERRYPICK,
             },
             {
+              type: ActionType.CHANGE,
+              key: ChangeActions.MOVE,
+            },
+            {
               type: ActionType.REVISION,
               key: RevisionActions.DOWNLOAD,
             },
@@ -624,6 +630,9 @@
         case ChangeActions.WIP:
           this._handleWipTap();
           break;
+        case ChangeActions.MOVE:
+          this._handleMoveTap();
+          break;
         default:
           this._fireAction(this._prependSlash(key), this.actions[key], false);
       }
@@ -717,6 +726,25 @@
       );
     },
 
+    _handleMoveConfirm() {
+      const el = this.$.confirmMove;
+      if (!el.branch) {
+        this.fire('show-alert', {message: ERR_BRANCH_EMPTY});
+        return;
+      }
+      this.$.overlay.close();
+      el.hidden = true;
+      this._fireAction(
+          '/move',
+          this.actions.move,
+          false,
+          {
+            destination_branch: el.branch,
+            message: el.message,
+          }
+      );
+    },
+
     _handleRevertDialogConfirm() {
       const el = this.$.confirmRevertDialog;
       this.$.overlay.close();
@@ -870,6 +898,12 @@
       this._showActionDialog(this.$.confirmCherrypick);
     },
 
+    _handleMoveTap() {
+      this.$.confirmMove.branch = '';
+      this.$.confirmMove.message = '';
+      this._showActionDialog(this.$.confirmMove);
+    },
+
     _handleDownloadTap() {
       this.fire('download-tap', null, {bubbles: false});
     },
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index 0e1726b..1f96c31 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -398,6 +398,34 @@
       });
     });
 
+    suite('move change', () => {
+      let fireActionStub;
+
+      setup(() => {
+        fireActionStub = sandbox.stub(element, '_fireAction');
+        sandbox.stub(window, 'alert');
+      });
+
+      test('works', () => {
+        element._handleMoveTap();
+
+        element._handleMoveConfirm();
+        assert.equal(fireActionStub.callCount, 0);
+
+        element.$.confirmMove.branch = 'master';
+        element._handleMoveConfirm();
+        assert.equal(fireActionStub.callCount, 1);
+      });
+
+      test('branch name cleared when re-open move', () => {
+        const emptyBranchName = '';
+        element.$.confirmMove.branch = 'master';
+
+        element._handleMoveTap();
+        assert.equal(element.$.confirmMove.branch, emptyBranchName);
+      });
+    });
+
     test('custom actions', done => {
       // Add a button with the same key as a server-based one to ensure
       // collisions are taken care of.
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
new file mode 100644
index 0000000..7260110
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.html
@@ -0,0 +1,87 @@
+<!--
+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.
+-->
+
+<link rel="import" href="../../../bower_components/iron-autogrow-textarea/iron-autogrow-textarea.html">
+<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../styles/shared-styles.html">
+<link rel="import" href="../../shared/gr-autocomplete/gr-autocomplete.html">
+<link rel="import" href="../../shared/gr-confirm-dialog/gr-confirm-dialog.html">
+<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
+
+<dom-module id="gr-confirm-move-dialog">
+  <template>
+    <style include="shared-styles">
+      :host {
+        display: block;
+        width: 30em;
+      }
+      :host([disabled]) {
+        opacity: .5;
+        pointer-events: none;
+      }
+      label {
+        cursor: pointer;
+      }
+      iron-autogrow-textarea {
+        padding: 0;
+      }
+      .main {
+        display: flex;
+        flex-direction: column;
+        width: 100%;
+      }
+      .main label,
+      .main input[type="text"] {
+        display: block;
+        font: inherit;
+        width: 100%;
+      }
+      .main .message {
+        border: groove;
+        width: 100%;
+      }
+    </style>
+    <gr-confirm-dialog
+        confirm-label="Move Change"
+        on-confirm="_handleConfirmTap"
+        on-cancel="_handleCancelTap">
+      <div class="header">Move Change to Another Branch</div>
+      <div class="main">
+        <label for="branchInput">
+          Move change to branch
+        </label>
+        <gr-autocomplete
+            id="branchInput"
+            text="{{branch}}"
+            query="[[_query]]"
+            placeholder="Destination branch">
+        </gr-autocomplete>
+        <label for="messageInput">
+          Move Change Commit Message
+        </label>
+        <iron-autogrow-textarea
+            id="messageInput"
+            class="message"
+            autocomplete="on"
+            rows="4"
+            max-rows="15"
+            bind-value="{{message}}"></iron-autogrow-textarea>
+      </div>
+    </gr-confirm-dialog>
+    <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
+  </template>
+  <script src="gr-confirm-move-dialog.js"></script>
+</dom-module>
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
new file mode 100644
index 0000000..6d35dbf
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog.js
@@ -0,0 +1,79 @@
+// 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.
+(function() {
+  'use strict';
+
+  const SUGGESTIONS_LIMIT = 15;
+
+  Polymer({
+    is: 'gr-confirm-move-dialog',
+
+    /**
+     * Fired when the confirm button is pressed.
+     *
+     * @event confirm
+     */
+
+    /**
+     * Fired when the cancel button is pressed.
+     *
+     * @event cancel
+     */
+
+    properties: {
+      branch: String,
+      message: String,
+      project: String,
+      _query: {
+        type: Function,
+        value() {
+          return this._getProjectBranchesSuggestions.bind(this);
+        },
+      },
+    },
+
+    _handleConfirmTap(e) {
+      e.preventDefault();
+      this.fire('confirm', null, {bubbles: false});
+    },
+
+    _handleCancelTap(e) {
+      e.preventDefault();
+      this.fire('cancel', null, {bubbles: false});
+    },
+
+    _getProjectBranchesSuggestions(input) {
+      if (input.startsWith('refs/heads/')) {
+        input = input.substring('refs/heads/'.length);
+      }
+      return this.$.restAPI.getProjectBranches(
+          input, this.project, SUGGESTIONS_LIMIT).then(response => {
+            const branches = [];
+            let branch;
+            for (const key in response) {
+              if (!response.hasOwnProperty(key)) { continue; }
+              if (response[key].ref.startsWith('refs/heads/')) {
+                branch = response[key].ref.substring('refs/heads/'.length);
+              } else {
+                branch = response[key].ref;
+              }
+              branches.push({
+                name: branch,
+              });
+            }
+            return branches;
+          });
+    },
+  });
+})();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
new file mode 100644
index 0000000..caf61ba
--- /dev/null
+++ b/polygerrit-ui/app/elements/change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<!--
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<title>gr-confirm-move-dialog</title>
+
+<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
+<script src="../../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../../test/common-test-setup.html"/>
+<link rel="import" href="gr-confirm-move-dialog.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+  <template>
+    <gr-confirm-move-dialog></gr-confirm-move-dialog>
+  </template>
+</test-fixture>
+
+<script>
+  suite('gr-confirm-move-dialog tests', () => {
+    let element;
+
+    setup(() => {
+      stub('gr-rest-api-interface', {
+        getProjectBranches(input) {
+          if (input.startsWith('test')) {
+            return Promise.resolve([
+              {
+                ref: 'refs/heads/test-branch',
+                revision: '67ebf73496383c6777035e374d2d664009e2aa5c',
+                can_delete: true,
+              },
+            ]);
+          } else {
+            return Promise.resolve({});
+          }
+        },
+      });
+      element = fixture('basic');
+      element.project = 'test-project';
+    });
+
+    test('with updated commit message', () => {
+      element.branch = 'master';
+      const myNewMessage = 'updated commit message';
+      element.message = myNewMessage;
+      flushAsynchronousOperations();
+      assert.equal(element.message, myNewMessage);
+    });
+
+    test('_getProjectBranchesSuggestions empty', done => {
+      element._getProjectBranchesSuggestions('nonexistent').then(branches => {
+        assert.equal(branches.length, 0);
+        done();
+      });
+    });
+
+    test('_getProjectBranchesSuggestions non-empty', done => {
+      element._getProjectBranchesSuggestions('test-branch').then(branches => {
+        assert.equal(branches.length, 1);
+        assert.equal(branches[0].name, 'test-branch');
+        done();
+      });
+    });
+  });
+</script>
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 5ab161c..05e658f 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
@@ -133,7 +133,7 @@
       'c': '_handleCKey',
       '[': '_handleLeftBracketKey',
       ']': '_handleRightBracketKey',
-      'o': '_handleOKey',
+      'o enter': '_handleOKey',
       'n': '_handleNKey',
       'p': '_handlePKey',
       'r': '_handleRKey',
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index 9325828..3f3350e 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -170,6 +170,10 @@
             </td>
             <td>Refresh list of changes</td>
           </tr>
+          <tr>
+            <td><span class="key">s</span></td>
+            <td>Star (or unstar) change</td>
+          </tr>
         </tbody>
         <!-- Dashboard -->
         <tbody hidden$="[[!_computeInView(view, 'dashboard')]]" hidden>
@@ -198,6 +202,10 @@
             </td>
             <td>Refresh list of changes</td>
           </tr>
+          <tr>
+            <td><span class="key">s</span></td>
+            <td>Star (or unstar) change</td>
+          </tr>
         </tbody>
         <!-- Change View -->
         <tbody hidden$="[[!_computeInView(view, 'change')]]" hidden>
@@ -243,7 +251,10 @@
             <td>Select previous file</td>
           </tr>
           <tr>
-            <td><span class="key">o</span></td>
+            <td>
+              <span class="key">Enter</span> or
+              <span class="key">o</span>
+            </td>
             <td>Show selected file</td>
           </tr>
           <tr>
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index d6e2e2f..713d771 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -72,7 +72,12 @@
       /** @type {Function} */
       _generateUrl: uninitialized,
 
-      /** @type {Function} */
+      /**
+       * Handler for when a legacy URL can be upgraded. Router implementations
+       * may implement as a no-op if route upgrades are not needed.
+       *
+       * @type {Function}
+       */
       _upgradeUrl: uninitialized,
 
       _checkPatchRange(patchNum, basePatchNum) {
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index 2296567..61251af 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -64,6 +64,13 @@
   const catchErrors = function(opt_context) {
     const context = opt_context || window;
     context.onerror = onError.bind(null, context.onerror);
+    context.addEventListener('unhandledrejection', e => {
+      const msg = e.reason.message;
+      const payload = {
+        error: e.reason,
+      };
+      GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload);
+    });
   };
   catchErrors();
 
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
index b99c5370..67cd329 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting_test.html
@@ -183,7 +183,12 @@
 
       setup(() => {
         reporter = sandbox.stub(GrReporting.prototype, 'reporter');
-        fakeWindow = {};
+        fakeWindow = {
+          handlers: {},
+          addEventListener(type, handler) {
+            this.handlers[type] = handler;
+          },
+        };
         sandbox.stub(console, 'error');
         window.GrReporting._catchErrors(fakeWindow);
       });
@@ -204,6 +209,15 @@
       test('prevent default event handler', () => {
         assert.isTrue(emulateThrow());
       });
+
+      test('unhandled rejection', () => {
+        fakeWindow.handlers['unhandledrejection']({
+          reason: {
+            message: 'bar',
+          },
+        });
+        assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-router/gr-router.js b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
index cc876fb..378744f 100644
--- a/polygerrit-ui/app/elements/core/gr-router/gr-router.js
+++ b/polygerrit-ui/app/elements/core/gr-router/gr-router.js
@@ -46,6 +46,13 @@
       page.base(base);
     }
 
+    /**
+     * While resolving Issue 6708, the need for some way to upgrade obsolete
+     * URLs in-place without page reloads became evident.
+     *
+     * This function aims to update the app params and the URL when the URL is
+     * found to be obsolete.
+     */
     const upgradeUrl = params => {
       const url = generateUrl(params);
       if (url !== window.location.pathname) {
@@ -283,6 +290,51 @@
       };
     });
 
+    // Matches /admin/plugins[,<offset>][/].
+    page(/^\/admin\/plugins(,(\d+))?(\/)?$/, loadUser, data => {
+      restAPI.getLoggedIn().then(loggedIn => {
+        if (loggedIn) {
+          app.params = {
+            view: Gerrit.Nav.View.ADMIN,
+            adminView: 'gr-plugin-list',
+            offset: data.params[1] || 0,
+            filter: null,
+          };
+        } else {
+          redirectToLogin(data.canonicalPath);
+        }
+      });
+    });
+
+    page('/admin/plugins/q/filter::filter,:offset', loadUser, data => {
+      restAPI.getLoggedIn().then(loggedIn => {
+        if (loggedIn) {
+          app.params = {
+            view: Gerrit.Nav.View.ADMIN,
+            adminView: 'gr-plugin-list',
+            offset: data.params.offset,
+            filter: data.params.filter,
+          };
+        } else {
+          redirectToLogin(data.canonicalPath);
+        }
+      });
+    });
+
+    page('/admin/plugins/q/filter::filter', loadUser, data => {
+      restAPI.getLoggedIn().then(loggedIn => {
+        if (loggedIn) {
+          app.params = {
+            view: Gerrit.Nav.View.ADMIN,
+            adminView: 'gr-plugin-list',
+            filter: data.params.filter || null,
+          };
+        } else {
+          redirectToLogin(data.canonicalPath);
+        }
+      });
+    });
+
     page(/^\/admin\/plugins(\/)?$/, loadUser, data => {
       restAPI.getLoggedIn().then(loggedIn => {
         if (loggedIn) {
@@ -335,7 +387,7 @@
     // /c/<project>/+/<changeNum>/[<basePatchNum>..][<patchNum>]/[path].
     // TODO(kaspern): Migrate completely to project based URLs, with backwards
     // compatibility for change-only.
-    page(/^\/c\/([^\/]+)\/\+\/(\d+)(\/?((\d+)(\.\.(\d+))?(\/(.+))?))?\/?$/,
+    page(/^\/c\/(.+)\/\+\/(\d+)(\/?((\d+)(\.\.(\d+))?(\/(.+))?))?\/?$/,
         ctx => {
           // Parameter order is based on the regex group number matched.
           const params = {
@@ -395,7 +447,7 @@
           data.params.view = Gerrit.Nav.View.AGREEMENTS;
           app.params = data.params;
         } else {
-          page.redirect('/login/' + encodeURIComponent(data.canonicalPath));
+          redirectToLogin(data.canonicalPath);
         }
       });
     });
@@ -408,7 +460,7 @@
             emailToken: data.params[0],
           };
         } else {
-          page.show('/login/' + encodeURIComponent(data.canonicalPath));
+          redirectToLogin(data.canonicalPath);
         }
       });
     });
@@ -418,7 +470,7 @@
         if (loggedIn) {
           app.params = {view: Gerrit.Nav.View.SETTINGS};
         } else {
-          page.show('/login/' + encodeURIComponent(data.canonicalPath));
+          redirectToLogin(data.canonicalPath);
         }
       });
     });
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 1865c4a..536bdb2 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
@@ -660,7 +660,7 @@
 
       return this.fetchJSON(
           `/projects/${encodeURIComponent(project)}/branches` +
-          `?n=${projectsBranchesPerPage + 1}&s=${offset}` +
+          `?n=${projectsBranchesPerPage + 1}&S=${offset}` +
           this._computeFilter(filter)
       );
     },
@@ -670,13 +670,18 @@
 
       return this.fetchJSON(
           `/projects/${encodeURIComponent(project)}/tags` +
-          `?n=${projectsTagsPerPage + 1}&s=${offset}` +
+          `?n=${projectsTagsPerPage + 1}&S=${offset}` +
           this._computeFilter(filter)
       );
     },
 
-    getPlugins() {
-      return this._fetchSharedCacheURL('/plugins/?all');
+    getPlugins(filter, pluginsPerPage, opt_offset) {
+      const offset = opt_offset || 0;
+
+      return this.fetchJSON(
+          `/plugins/?all&n=${pluginsPerPage + 1}&S=${offset}` +
+          this._computeFilter(filter)
+      );
     },
 
     getSuggestedGroups(inputVal, opt_n, opt_errFn, opt_ctx) {
diff --git a/polygerrit-ui/app/polylint_test.sh b/polygerrit-ui/app/polylint_test.sh
index 8f0d80f..ca9f9a9 100755
--- a/polygerrit-ui/app/polylint_test.sh
+++ b/polygerrit-ui/app/polylint_test.sh
@@ -15,6 +15,6 @@
     exit 1
 fi
 
-unzip polygerrit-ui/polygerrit_components.bower_components.zip -d polygerrit-ui/app
+unzip -o polygerrit-ui/polygerrit_components.bower_components.zip -d polygerrit-ui/app
 
 ${polylint_bin} --root polygerrit-ui/app --input elements/gr-app.html --b 'bower_components'
\ No newline at end of file
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index 9678deb..355a7b6 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -51,6 +51,7 @@
     'change/gr-comment-list/gr-comment-list_test.html',
     'change/gr-commit-info/gr-commit-info_test.html',
     'change/gr-confirm-cherrypick-dialog/gr-confirm-cherrypick-dialog_test.html',
+    'change/gr-confirm-move-dialog/gr-confirm-move-dialog_test.html',
     'change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.html',
     'change/gr-confirm-revert-dialog/gr-confirm-revert-dialog_test.html',
     'change/gr-download-dialog/gr-download-dialog_test.html',
diff --git a/tools/bzl/gwt.bzl b/tools/bzl/gwt.bzl
index deeb5d5..ef182bf 100644
--- a/tools/bzl/gwt.bzl
+++ b/tools/bzl/gwt.bzl
@@ -52,10 +52,6 @@
     "-XdisableCastChecking",
 ]
 
-PLUGIN_DEPS_NEVERLINK = [
-    "//gerrit-plugin-api:lib-neverlink",
-]
-
 GWT_PLUGIN_DEPS_NEVERLINK = [
     "//gerrit-plugin-gwtui:gwtui-api-lib-neverlink",
     "//lib/gwt:user-neverlink",
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl
index 59e7335..11ac572 100644
--- a/tools/bzl/plugin.bzl
+++ b/tools/bzl/plugin.bzl
@@ -5,12 +5,12 @@
     "GWT_PLUGIN_DEPS_NEVERLINK",
     "GWT_TRANSITIVE_DEPS",
     "GWT_COMPILER_ARGS",
-    "PLUGIN_DEPS_NEVERLINK",
     "GWT_JVM_ARGS",
     "gwt_binary",
 )
 
 PLUGIN_DEPS = ["//gerrit-plugin-api:lib"]
+PLUGIN_DEPS_NEVERLINK = ["//gerrit-plugin-api:lib-neverlink"]
 
 PLUGIN_TEST_DEPS = [
     "//gerrit-acceptance-framework:lib",