Merge "Remove polyfilled image API"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 67eac4c..c029031 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -2521,9 +2521,12 @@
 of an Apache HTTP proxy layer as security enforcement on top of Gerrit
 by returning a trusted username as HTTP Header.
 +
+Allow multiple values to install multiple servlet filters.
++
 Example of using a security library secure.jar under $GERRIT_SITE/lib
-that provides a org.anyorg.MySecureFilter Servlet Filter that enforces
-a trusted username in the `TRUSTED_USER` HTTP Header:
+that provides a org.anyorg.MySecureHeaderFilter Servlet Filter that enforces
+a trusted username in the `TRUSTED_USER` HTTP Header and
+org.anyorg.MySecureIPFilter that performs source IP security filtering:
 
 ----
 [auth]
@@ -2531,9 +2534,25 @@
 	httpHeader = TRUSTED_USER
 
 [httpd]
-	filterClass = org.anyorg.MySecureFilter
+	filterClass = org.anyorg.MySecureHeaderFilter
+	filterClass = org.anyorg.MySecureIPFilter
 ----
 
+[[httpd.idleTimeout]]httpd.idleTimeout::
++
+Maximum idle time for a connection, which roughly translates to the
+TCP socket `SO_TIMEOUT`.
++
+The max idle time is applied:
+* When waiting for a new message to be received on a connection
+* When waiting for a new message to be sent on a connection
++
+This value is interpreted as the maximum time between some progress
+being made on the connection. So if a single byte is read or written,
+then the timeout is reset.
++
+By default, 30 seconds.
+
 [[httpd.robotsFile]]httpd.robotsFile::
 +
 Location of an external robots.txt file to be used instead of the one
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 64b1c0d..7da501a 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -398,6 +398,10 @@
 +
 Update of the account secondary index
 
+* `com.google.gerrit.server.extensions.events.GroupIndexedListener`:
++
+Update of the group secondary index
+
 * `com.google.gerrit.httpd.WebLoginListener`:
 +
 User login or logout interactively on the Web user interface.
diff --git a/Documentation/intro-user.txt b/Documentation/intro-user.txt
index 5dcd947..2c42d74 100644
--- a/Documentation/intro-user.txt
+++ b/Documentation/intro-user.txt
@@ -523,6 +523,18 @@
 In that case, care should be taken to prevent the CI system from
 exposing secret details.
 
+[[ignore]]
+== Ignoring and Muting Changes
+
+Changes can be ignored, which means they will not appear in the 'Incoming
+Reviews' dashboard and any related email notifications will be suppressed.
+This can be useful when you are added as a reviewer to a change on which
+you do not actively participate in the review, but do not want to completely
+remove yourself.
+
+Alternatively, rather than completely ignoring the change, it can be muted.
+Muting a change means it will always be marked as "reviewed" in dashboards,
+until a new patch set is uploaded.
 
 [[drafts]]
 == Working with Drafts
@@ -536,12 +548,15 @@
 changes can also be used to backup unfinished changes.
 
 A draft change is created by pushing to the magic
-`refs/drafts/<target-branch>` ref.
+`refs/drafts/<target-branch>` ref, or by pushing with the 'draft'
+option to `refs/for/<target-branch>%draft`.
 
 .Push a Draft Change
 ----
   $ git commit
   $ git push origin HEAD:refs/drafts/master
+  # or
+  $ git push origin HEAD:refs/for/master%draft
 ----
 
 Draft changes have the state link:user-review-ui.html#draft[Draft] and
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index da30264..b7c50f4 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -2166,14 +2166,23 @@
 [[mark-private]]
 === Mark Private
 --
-'PUT /changes/link:#change-id[\{change-id\}]/private'
+'POST /changes/link:#change-id[\{change-id\}]/private'
 --
 
-Marks the change to be private. Note users can only mark own changes as private.
+Marks the change to be private. Changes may only be marked private by the
+owner or site administrators.
+
+A message can be specified in the request body inside a
+link:#private-input[PrivateInput] entity.
 
 .Request
 ----
-  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/private HTTP/1.0
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/private HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "message": "After this security fix has been released we can make it public now."
+  }
 ----
 
 .Response
@@ -2192,9 +2201,17 @@
 Marks the change to be non-private. Note users can only unmark own private
 changes.
 
+A message can be specified in the request body inside a
+link:#private-input[PrivateInput] entity.
+
 .Request
 ----
   DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/private HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "message": "This is a security fix that must not be public."
+  }
 ----
 
 .Response
@@ -2204,6 +2221,20 @@
 
 If the change was already not private, the response is "`409 Conflict`".
 
+Please note that some proxies prohibit request bodies for DELETE
+requests. In this case, if you want to set a message options, use a
+POST request:
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/private.delete HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "message": "This is a security fix that must not be public."
+  }
+----
+
 [[ignore]]
 === Ignore
 --
@@ -4394,6 +4425,62 @@
   }
 ----
 
+[[delete-comment]]
+=== Delete Comment
+--
+'DELETE /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/comments/link:#comment-id[\{comment-id\}]' +
+'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/comments/link:#comment-id[\{comment-id\}]/delete'
+--
+
+Deletes a published comment of a revision. Instead of deleting the
+whole comment, this endpoint just replaces the comment's message
+with a new message, which contains the name of the user who deletes
+the comment and the reason why it's deleted. The reason can be
+provided in the request body as a
+link:#delete-comment-input[DeleteCommentInput] entity.
+
+Note that only users with the
+link:access-control.html#capability_administrateServer[Administrate Server]
+global capability are permitted to delete a comment.
+
+Please note that some proxies prohibit request bodies for DELETE
+requests. In this case, if you want to specify options, use a
+POST request:
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/comments/TvcXrmjM/delete HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "reason": "contains confidential information"
+  }
+----
+
+As response a link:#comment-info[CommentInfo] entity is returned that
+describes the updated comment.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json; charset=UTF-8
+
+  )]}'
+  {
+    "id": "TvcXrmjM",
+    "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+    "line": 23,
+    "message": "Comment removed by: Administrator; Reason: contains confidential information",
+    "updated": "2013-02-26 15:40:43.986000000",
+    "author": {
+      "_account_id": 1000096,
+      "name": "John Doe",
+      "email": "john.doe@example.com"
+    }
+  }
+----
+
 [[list-robot-comments]]
 === List Robot Comments
 --
@@ -5739,6 +5826,20 @@
 link:#web-link-info[WebLinkInfo] entities.
 |===========================
 
+[[delete-comment-input]]
+=== DeleteCommentInput
+The `DeleteCommentInput` entity contains the option for deleting a comment.
+
+[options="header",cols="1,^1,5"]
+|=============================
+|Field Name               ||Description
+|`reason`                 |optional|
+The reason why the comment should be deleted. +
+If set, the comment's message will be replaced with
+"Comment removed by: `name`; Reason: `reason`",
+or just "Comment removed by: `name`." if not set.
+|=============================
+
 [[delete-reviewer-input]]
 === DeleteReviewerInput
 The `DeleteReviewerInput` entity contains options for the deletion of a
@@ -6221,6 +6322,17 @@
 identify the accounts that should be should be notified.
 |=======================
 
+[[private-input]]
+=== PrivateInput
+The `PrivateInput` entity contains information for changing the private
+flag on a change.
+
+[options="header",cols="1,^1,5"]
+|=======================
+|Field Name||Description
+|`message` |optional|Message describing why the private flag was changed.
+|=======================
+
 [[problem-info]]
 === ProblemInfo
 The `ProblemInfo` entity contains a description of a potential consistency problem
diff --git a/WORKSPACE b/WORKSPACE
index 3b46d22..8cb061e 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -6,9 +6,9 @@
 
 http_archive(
     name = "io_bazel_rules_closure",
-    strip_prefix = "rules_closure-0.4.1",
-    sha256 = "ba5e2e10cdc4027702f96e9bdc536c6595decafa94847d08ae28c6cb48225124",
-    url = "http://bazel-mirror.storage.googleapis.com/github.com/bazelbuild/rules_closure/archive/0.4.1.tar.gz",
+    sha256 = "af1f5a31b8306faed9d09a38c8e2c1d6afc4c4a2dada3b5de11cceae8c7f4596",
+    strip_prefix = "rules_closure-f68d4b5a55c04ee50a3196590dce1ca8e7dbf438",
+    url = "https://bazel-mirror.storage.googleapis.com/github.com/bazelbuild/rules_closure/archive/f68d4b5a55c04ee50a3196590dce1ca8e7dbf438.tar.gz",  # 2017-05-05
 )
 
 # File is specific to Polymer and copied from the Closure Github -- should be
@@ -24,18 +24,9 @@
 
 # Prevent redundant loading of dependencies.
 closure_repositories(
-    omit_aopalliance=True,
-    omit_args4j=True,
-    omit_jsr305=True,
-    omit_gson=True,
-    omit_guava=True,
-    omit_guice=True,
-    omit_soy=True,
-    omit_icu4j=True,
-    omit_asm=True,
-    omit_asm_analysis=True,
-    omit_asm_commons=True,
-    omit_asm_util=True,
+    omit_aopalliance = True,
+    omit_args4j = True,
+    omit_javax_inject = True,
 )
 
 ANTLR_VERS = "3.5.2"
@@ -109,18 +100,18 @@
     sha1 = "5d9e2e895e3111622720157d0aa540066d5fce3a",
 )
 
-GWT_VERS = "2.8.0"
+GWT_VERS = "2.8.1"
 
 maven_jar(
     name = "user",
     artifact = "com.google.gwt:gwt-user:" + GWT_VERS,
-    sha1 = "518579870499e15531f454f35dca0772d7fa31f7",
+    sha1 = "9a13fbee70848f1f1cddd3ae33ad180af3392d9e",
 )
 
 maven_jar(
     name = "dev",
     artifact = "com.google.gwt:gwt-dev:" + GWT_VERS,
-    sha1 = "f160a61272c5ebe805cd2d3d3256ed3ecf14893f",
+    sha1 = "c7e88c07e9cda90cc623b4451d0d9713ae03aa53",
 )
 
 maven_jar(
@@ -203,8 +194,8 @@
 
 maven_jar(
     name = "joda_time",
-    artifact = "joda-time:joda-time:2.9.8",
-    sha1 = "03986e1763e5df02ad7fc040ecb555193a8436bb",
+    artifact = "joda-time:joda-time:2.9.9",
+    sha1 = "f7b520c458572890807d143670c9b24f4de90897",
 )
 
 maven_jar(
@@ -668,6 +659,9 @@
     sha1 = "fd369423346b2f1525c413e33f8cf95b09c92cbd",
 )
 
+# Note that all of the following org.apache.httpcomponents have newer versions,
+# but 4.4.1 is the only version that is available for all of them.
+# TODO: Check what combination of new versions are compatible.
 HTTPCOMP_VERS = "4.4.1"
 
 maven_jar(
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
index 711b8cf..d0f998c 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -109,7 +109,7 @@
       if (email != null) {
         extIds.add(ExternalId.createEmail(id, email));
       }
-      externalIdsUpdate.create().insert(db, extIds);
+      externalIdsUpdate.create().insert(extIds);
 
       Account a = new Account(id, TimeUtil.nowTs());
       a.setFullName(fullName);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index a3ca832..ec4ff52 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -168,9 +168,9 @@
       // savedExternalIds is null when we don't run SSH tests and the assume in
       // @Before in AbstractDaemonTest prevents this class' @Before method from
       // being executed.
-      externalIdsUpdate.delete(db, getExternalIds(admin));
-      externalIdsUpdate.delete(db, getExternalIds(user));
-      externalIdsUpdate.insert(db, savedExternalIds);
+      externalIdsUpdate.delete(getExternalIds(admin));
+      externalIdsUpdate.delete(getExternalIds(user));
+      externalIdsUpdate.insert(savedExternalIds);
     }
     accountCache.evict(admin.getId());
     accountCache.evict(user.getId());
@@ -533,7 +533,7 @@
         ImmutableList.of(
             ExternalId.createWithEmail(ExternalId.Key.parse(extId1), admin.id, email),
             ExternalId.createWithEmail(ExternalId.Key.parse(extId2), admin.id, email));
-    externalIdsUpdateFactory.create().insert(db, extIds);
+    externalIdsUpdateFactory.create().insert(extIds);
     accountCache.evict(admin.id);
     accountIndexedCounter.assertReindexOf(admin);
     assertThat(
@@ -588,7 +588,7 @@
     String email = "foo.bar@example.com";
     externalIdsUpdateFactory
         .create()
-        .insert(db, ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id, email));
+        .insert(ExternalId.createWithEmail(ExternalId.Key.parse("foo:bar"), admin.id, email));
     accountCache.evict(admin.id);
     assertEmail(byEmailCache.get(email), admin);
 
@@ -830,7 +830,7 @@
   public void addOtherUsersGpgKey_Conflict() throws Exception {
     // Both users have a matching external ID for this key.
     addExternalIdEmail(admin, "test5@example.com");
-    externalIdsUpdate.insert(db, ExternalId.create("foo", "myId", user.getId()));
+    externalIdsUpdate.insert(ExternalId.create("foo", "myId", user.getId()));
     accountCache.evict(user.getId());
     accountIndexedCounter.assertReindexOf(user);
 
@@ -1001,9 +1001,9 @@
     return new String(out.toByteArray(), UTF_8);
   }
 
-  @SuppressWarnings({"unchecked", "rawtypes"})
-  private static void assertIteratorSize(int size, Iterator it) {
-    assertThat(ImmutableList.copyOf(it)).hasSize(size);
+  private static void assertIteratorSize(int size, Iterator<?> it) {
+    List<?> lst = ImmutableList.copyOf(it);
+    assertThat(lst).hasSize(size);
   }
 
   private static void assertKeyMapContains(TestKey expected, Map<String, GpgKeyInfo> actualMap) {
@@ -1043,7 +1043,7 @@
         expected.transform(k -> BaseEncoding.base16().encode(k.getPublicKey().getFingerprint()));
     Iterable<String> actualFps =
         externalIds
-            .byAccount(db, currAccountId, SCHEME_GPGKEY)
+            .byAccount(currAccountId, SCHEME_GPGKEY)
             .stream()
             .map(e -> e.key().id())
             .collect(toSet());
@@ -1072,7 +1072,7 @@
   private void addExternalIdEmail(TestAccount account, String email) throws Exception {
     checkNotNull(email);
     externalIdsUpdate.insert(
-        db, ExternalId.createWithEmail(name("test"), email, account.getId(), email));
+        ExternalId.createWithEmail(name("test"), email, account.getId(), email));
     // Clear saved AccountState and ExternalIds.
     accountCache.evict(account.getId());
     accountIndexedCounter.assertReindexOf(account);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
index a4a3c04..5d0f0ba 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -191,29 +191,55 @@
     String changeId = result.getChangeId();
     assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
 
-    gApi.changes().id(changeId).setPrivate(true);
+    gApi.changes().id(changeId).setPrivate(true, null);
     ChangeInfo info = gApi.changes().id(changeId).get();
     assertThat(info.isPrivate).isTrue();
     assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set private");
     assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_PRIVATE);
 
-    gApi.changes().id(changeId).setPrivate(false);
+    gApi.changes().id(changeId).setPrivate(false, null);
     info = gApi.changes().id(changeId).get();
     assertThat(info.isPrivate).isNull();
     assertThat(Iterables.getLast(info.messages).message).isEqualTo("Unset private");
     assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_UNSET_PRIVATE);
+
+    String msg = "This is a security fix that must not be public.";
+    gApi.changes().id(changeId).setPrivate(true, msg);
+    info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isTrue();
+    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Set private\n\n" + msg);
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_SET_PRIVATE);
+
+    msg = "After this security fix has been released we can make it public now.";
+    gApi.changes().id(changeId).setPrivate(false, msg);
+    info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isNull();
+    assertThat(Iterables.getLast(info.messages).message).isEqualTo("Unset private\n\n" + msg);
+    assertThat(Iterables.getLast(info.messages).tag).contains(ChangeMessagesUtil.TAG_UNSET_PRIVATE);
   }
 
   @Test
-  public void setPrivateByOtherUser() throws Exception {
+  public void administratorCanSetUserChangePrivate() throws Exception {
     TestRepository<InMemoryRepository> userRepo = cloneProject(project, user);
     PushOneCommit.Result result =
         pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master");
 
-    assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isNull();
+    String changeId = result.getChangeId();
+    assertThat(gApi.changes().id(changeId).get().isPrivate).isNull();
+
+    gApi.changes().id(changeId).setPrivate(true, null);
+    setApiUser(user);
+    ChangeInfo info = gApi.changes().id(changeId).get();
+    assertThat(info.isPrivate).isTrue();
+  }
+
+  @Test
+  public void cannotSetOtherUsersChangePrivate() throws Exception {
+    PushOneCommit.Result result = createChange();
+    setApiUser(user);
     exception.expect(AuthException.class);
     exception.expectMessage("not allowed to mark private");
-    gApi.changes().id(result.getChangeId()).setPrivate(true);
+    gApi.changes().id(result.getChangeId()).setPrivate(true, null);
   }
 
   @Test
@@ -223,7 +249,7 @@
         pushFactory.create(db, user.getIdent(), userRepo).to("refs/for/master");
 
     setApiUser(user);
-    gApi.changes().id(result.getChangeId()).setPrivate(true);
+    gApi.changes().id(result.getChangeId()).setPrivate(true, null);
     // Owner can always access its private changes.
     assertThat(gApi.changes().id(result.getChangeId()).get().isPrivate).isTrue();
 
@@ -246,7 +272,7 @@
   @Test
   public void privateChangeOfOtherUserCanBeAccessedWithPermission() throws Exception {
     PushOneCommit.Result result = createChange();
-    gApi.changes().id(result.getChangeId()).setPrivate(true);
+    gApi.changes().id(result.getChangeId()).setPrivate(true, null);
 
     allow(Permission.VIEW_PRIVATE_CHANGES, REGISTERED_USERS, "refs/*");
     setApiUser(user);
@@ -596,6 +622,50 @@
   }
 
   @Test
+  public void rebaseNotAllowedWithoutPushPermission() throws Exception {
+    // Create two changes both with the same parent
+    PushOneCommit.Result r = createChange();
+    testRepo.reset("HEAD~1");
+    PushOneCommit.Result r2 = createChange();
+
+    // Approve and submit the first change
+    RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+    revision.review(ReviewInput.approve());
+    revision.submit();
+
+    grant(Permission.REBASE, project, "refs/heads/master", false, REGISTERED_USERS);
+    block(Permission.PUSH, REGISTERED_USERS, "refs/for/*");
+
+    // Rebase the second
+    String changeId = r2.getChangeId();
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    exception.expectMessage("rebase not permitted");
+    gApi.changes().id(changeId).rebase();
+  }
+
+  @Test
+  public void rebaseNotAllowedForOwnerWithoutPushPermission() throws Exception {
+    // Create two changes both with the same parent
+    PushOneCommit.Result r = createChange();
+    testRepo.reset("HEAD~1");
+    PushOneCommit.Result r2 = createChange();
+
+    // Approve and submit the first change
+    RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
+    revision.review(ReviewInput.approve());
+    revision.submit();
+
+    block(Permission.PUSH, REGISTERED_USERS, "refs/for/*");
+
+    // Rebase the second
+    String changeId = r2.getChangeId();
+    exception.expect(AuthException.class);
+    exception.expectMessage("rebase not permitted");
+    gApi.changes().id(changeId).rebase();
+  }
+
+  @Test
   public void publish() throws Exception {
     PushOneCommit.Result r = createChange("refs/drafts/master");
     assertThat(info(r.getChangeId()).status).isEqualTo(ChangeStatus.DRAFT);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 0abdde6..3b868dd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -518,7 +518,7 @@
           lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
       assertWithMessage("Precondition violated").that(initialRefNames).contains(change3RefName);
 
-      gApi.changes().id(c3.getId().get()).setPrivate(true);
+      gApi.changes().id(c3.getId().get()).setPrivate(true, null);
 
       List<String> refNames = lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
       assertThat(refNames).doesNotContain(change3RefName);
@@ -538,7 +538,7 @@
           lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
       assertWithMessage("Precondition violated").that(initialRefNames).contains(change3RefName);
 
-      gApi.changes().id(c3.getId().get()).setPrivate(true);
+      gApi.changes().id(c3.getId().get()).setPrivate(true, null);
 
       List<String> refNames = lsRemoteCommand.call().stream().map(Ref::getName).collect(toList());
       assertThat(refNames).contains(change3RefName);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
index f6c70b0..77d02fb 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/ExternalIdIT.java
@@ -16,10 +16,13 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.acceptance.GitUtil.fetch;
+import static com.google.gerrit.acceptance.GitUtil.pushHead;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_UUID;
 import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.stream.Collectors.toList;
 import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
 import static org.junit.Assert.fail;
 
@@ -28,9 +31,9 @@
 import com.github.rholder.retry.RetryerBuilder;
 import com.github.rholder.retry.StopStrategies;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.GerritConfig;
-import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.acceptance.Sandboxed;
 import com.google.gerrit.common.data.GlobalCapability;
@@ -53,6 +56,7 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.LockFailureException;
 import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.io.IOException;
@@ -75,6 +79,9 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
 import org.eclipse.jgit.util.MutableInteger;
 import org.junit.Test;
 
@@ -90,15 +97,7 @@
   public void getExternalIds() throws Exception {
     Collection<ExternalId> expectedIds = accountCache.get(user.getId()).getExternalIds();
 
-    List<AccountExternalIdInfo> expectedIdInfos = new ArrayList<>();
-    for (ExternalId id : expectedIds) {
-      AccountExternalIdInfo info = new AccountExternalIdInfo();
-      info.identity = id.key().get();
-      info.emailAddress = id.email();
-      info.canDelete = !id.isScheme(SCHEME_USERNAME) ? true : null;
-      info.trusted = true;
-      expectedIdInfos.add(info);
-    }
+    List<AccountExternalIdInfo> expectedIdInfos = toExternalIdInfos(expectedIds);
 
     RestResponse response = userRestSession.get("/accounts/self/external.ids");
     response.assertOK();
@@ -197,19 +196,126 @@
 
   @Test
   public void pushToExternalIdsBranch() throws Exception {
-    grant(Permission.READ, allUsers, RefNames.REFS_EXTERNAL_IDS);
-    grant(Permission.PUSH, allUsers, RefNames.REFS_EXTERNAL_IDS);
-
     TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
-    fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":externalIds");
-    allUsersRepo.reset("externalIds");
-    PushOneCommit push = pushFactory.create(db, admin.getIdent(), allUsersRepo);
-    push.to(RefNames.REFS_EXTERNAL_IDS)
-        .assertErrorStatus("not allowed to update " + RefNames.REFS_EXTERNAL_IDS);
+    fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
+    allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
+
+    // different case email is allowed
+    ExternalId newExtId = createExternalIdWithOtherCaseEmail("foo:bar");
+    addExtId(allUsersRepo, newExtId);
+    allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
+
+    List<AccountExternalIdInfo> extIdsBefore = gApi.accounts().self().getExternalIds();
+
+    allowPushOfExternalIds();
+    PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
+    assertThat(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS).getStatus()).isEqualTo(Status.OK);
+
+    List<AccountExternalIdInfo> extIdsAfter = gApi.accounts().self().getExternalIds();
+    assertThat(extIdsAfter)
+        .containsExactlyElementsIn(
+            Iterables.concat(extIdsBefore, ImmutableSet.of(toExternalIdInfo(newExtId))));
   }
 
   @Test
-  @GerritConfig(name = "user.readExternalIdsFromGit", value = "true")
+  public void pushToExternalIdsBranchRejectsExternalIdWithoutAccountId() throws Exception {
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
+    allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
+
+    insertExternalIdWithoutAccountId(
+        allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar");
+    allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
+
+    allowPushOfExternalIds();
+    PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
+    assertRefUpdateFailure(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS), "invalid external IDs");
+  }
+
+  @Test
+  public void pushToExternalIdsBranchRejectsExternalIdWithKeyThatDoesntMatchTheNoteId()
+      throws Exception {
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
+    allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
+
+    insertExternalIdWithKeyThatDoesntMatchNoteId(
+        allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar");
+    allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
+
+    allowPushOfExternalIds();
+    PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
+    assertRefUpdateFailure(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS), "invalid external IDs");
+  }
+
+  @Test
+  public void pushToExternalIdsBranchRejectsExternalIdWithInvalidConfig() throws Exception {
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
+    allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
+
+    insertExternalIdWithInvalidConfig(
+        allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar");
+    allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
+
+    allowPushOfExternalIds();
+    PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
+    assertRefUpdateFailure(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS), "invalid external IDs");
+  }
+
+  @Test
+  public void pushToExternalIdsBranchRejectsExternalIdWithEmptyNote() throws Exception {
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
+    allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
+
+    insertExternalIdWithEmptyNote(
+        allUsersRepo.getRepository(), allUsersRepo.getRevWalk(), "foo:bar");
+    allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
+
+    allowPushOfExternalIds();
+    PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
+    assertRefUpdateFailure(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS), "invalid external IDs");
+  }
+
+  @Test
+  public void pushToExternalIdsBranchRejectsExternalIdForNonExistingAccount() throws Exception {
+    testPushToExternalIdsBranchRejectsInvalidExternalId(
+        createExternalIdForNonExistingAccount("foo:bar"));
+  }
+
+  @Test
+  public void pushToExternalIdsBranchRejectsExternalIdWithInvalidEmail() throws Exception {
+    testPushToExternalIdsBranchRejectsInvalidExternalId(
+        createExternalIdWithInvalidEmail("foo:bar"));
+  }
+
+  @Test
+  public void pushToExternalIdsBranchRejectsDuplicateEmails() throws Exception {
+    testPushToExternalIdsBranchRejectsInvalidExternalId(
+        createExternalIdWithDuplicateEmail("foo:bar"));
+  }
+
+  @Test
+  public void pushToExternalIdsBranchRejectsBadPassword() throws Exception {
+    testPushToExternalIdsBranchRejectsInvalidExternalId(createExternalIdWithBadPassword("foo"));
+  }
+
+  private void testPushToExternalIdsBranchRejectsInvalidExternalId(ExternalId invalidExtId)
+      throws Exception {
+    TestRepository<InMemoryRepository> allUsersRepo = cloneProject(allUsers);
+    fetch(allUsersRepo, RefNames.REFS_EXTERNAL_IDS + ":" + RefNames.REFS_EXTERNAL_IDS);
+    allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
+
+    addExtId(allUsersRepo, invalidExtId);
+    allUsersRepo.reset(RefNames.REFS_EXTERNAL_IDS);
+
+    allowPushOfExternalIds();
+    PushResult r = pushHead(allUsersRepo, RefNames.REFS_EXTERNAL_IDS);
+    assertRefUpdateFailure(r.getRemoteUpdate(RefNames.REFS_EXTERNAL_IDS), "invalid external IDs");
+  }
+
+  @Test
   public void readExternalIdsWhenInvalidExternalIdsExist() throws Exception {
     allowGlobalCapabilities(REGISTERED_USERS, GlobalCapability.ACCESS_DATABASE);
     resetCurrentApiUser();
@@ -217,15 +323,15 @@
     insertValidExternalIds();
     insertInvalidButParsableExternalIds();
 
-    Set<ExternalId> parseableExtIds = externalIds.all(db);
+    Set<ExternalId> parseableExtIds = externalIds.all();
 
     insertNonParsableExternalIds();
 
-    Set<ExternalId> extIds = externalIds.all(db);
+    Set<ExternalId> extIds = externalIds.all();
     assertThat(extIds).containsExactlyElementsIn(parseableExtIds);
 
     for (ExternalId parseableExtId : parseableExtIds) {
-      ExternalId extId = externalIds.get(db, parseableExtId.key());
+      ExternalId extId = externalIds.get(parseableExtId.key());
       assertThat(extId).isEqualTo(parseableExtId);
     }
   }
@@ -270,13 +376,12 @@
 
     // create valid external IDs
     u.insert(
-        db,
         ExternalId.createWithPassword(
             ExternalId.Key.parse(nextId(scheme, i)),
             admin.id,
             "admin.other@example.com",
             "secret-password"));
-    u.insert(db, createExternalIdWithOtherCaseEmail(nextId(scheme, i)));
+    u.insert(createExternalIdWithOtherCaseEmail(nextId(scheme, i)));
   }
 
   private Set<ConsistencyProblemInfo> insertInvalidButParsableExternalIds()
@@ -288,7 +393,7 @@
     Set<ConsistencyProblemInfo> expectedProblems = new HashSet<>();
     ExternalId extIdForNonExistingAccount =
         createExternalIdForNonExistingAccount(nextId(scheme, i));
-    u.insert(db, extIdForNonExistingAccount);
+    u.insert(extIdForNonExistingAccount);
     expectedProblems.add(
         consistencyError(
             "External ID '"
@@ -297,7 +402,7 @@
                 + extIdForNonExistingAccount.accountId().get()));
 
     ExternalId extIdWithInvalidEmail = createExternalIdWithInvalidEmail(nextId(scheme, i));
-    u.insert(db, extIdWithInvalidEmail);
+    u.insert(extIdWithInvalidEmail);
     expectedProblems.add(
         consistencyError(
             "External ID '"
@@ -306,7 +411,7 @@
                 + extIdWithInvalidEmail.email()));
 
     ExternalId extIdWithDuplicateEmail = createExternalIdWithDuplicateEmail(nextId(scheme, i));
-    u.insert(db, extIdWithDuplicateEmail);
+    u.insert(extIdWithDuplicateEmail);
     expectedProblems.add(
         consistencyError(
             "Email '"
@@ -318,7 +423,7 @@
                 + "'"));
 
     ExternalId extIdWithBadPassword = createExternalIdWithBadPassword("admin-username");
-    u.insert(db, extIdWithBadPassword);
+    u.insert(extIdWithBadPassword);
     expectedProblems.add(
         consistencyError(
             "External ID '"
@@ -508,7 +613,7 @@
             () -> {
               if (!doneBgUpdate.getAndSet(true)) {
                 try {
-                  extIdsUpdate.create().insert(db, ExternalId.create(barId, admin.id));
+                  extIdsUpdate.create().insert(ExternalId.create(barId, admin.id));
                 } catch (IOException | ConfigInvalidException | OrmException e) {
                   // Ignore, the successful insertion of the external ID is asserted later
                 }
@@ -516,11 +621,11 @@
             },
             retryer);
     assertThat(doneBgUpdate.get()).isFalse();
-    update.insert(db, ExternalId.create(fooId, admin.id));
+    update.insert(ExternalId.create(fooId, admin.id));
     assertThat(doneBgUpdate.get()).isTrue();
 
-    assertThat(externalIds.get(db, fooId)).isNotNull();
-    assertThat(externalIds.get(db, barId)).isNotNull();
+    assertThat(externalIds.get(fooId)).isNotNull();
+    assertThat(externalIds.get(barId)).isNotNull();
   }
 
   @Test
@@ -544,7 +649,7 @@
               try {
                 extIdsUpdate
                     .create()
-                    .insert(db, ExternalId.create(extIdsKeys[bgCounter.getAndAdd(1)], admin.id));
+                    .insert(ExternalId.create(extIdsKeys[bgCounter.getAndAdd(1)], admin.id));
               } catch (IOException | ConfigInvalidException | OrmException e) {
                 // Ignore, the successful insertion of the external ID is asserted later
               }
@@ -555,14 +660,14 @@
                 .build());
     assertThat(bgCounter.get()).isEqualTo(0);
     try {
-      update.insert(db, ExternalId.create(ExternalId.Key.create("abc", "abc"), admin.id));
+      update.insert(ExternalId.create(ExternalId.Key.create("abc", "abc"), admin.id));
       fail("expected LockFailureException");
     } catch (LockFailureException e) {
       // Ignore, expected
     }
     assertThat(bgCounter.get()).isEqualTo(extIdsKeys.length);
     for (ExternalId.Key extIdKey : extIdsKeys) {
-      assertThat(externalIds.get(db, extIdKey)).isNotNull();
+      assertThat(externalIds.get(extIdKey)).isNotNull();
     }
   }
 
@@ -570,38 +675,36 @@
   public void readExternalIdWithAccountIdThatCanBeExpressedInKiB() throws Exception {
     ExternalId.Key extIdKey = ExternalId.Key.parse("foo:bar");
     Account.Id accountId = new Account.Id(1024 * 100);
-    extIdsUpdate.create().insert(db, ExternalId.create(extIdKey, accountId));
-    ExternalId extId = externalIds.get(db, extIdKey);
+    extIdsUpdate.create().insert(ExternalId.create(extIdKey, accountId));
+    ExternalId extId = externalIds.get(extIdKey);
     assertThat(extId.accountId()).isEqualTo(accountId);
   }
 
   @Test
-  @GerritConfig(name = "user.readExternalIdsFromGit", value = "true")
   public void checkNoReloadAfterUpdate() throws Exception {
-    Set<ExternalId> expectedExtIds = new HashSet<>(externalIds.byAccount(db, admin.id));
+    Set<ExternalId> expectedExtIds = new HashSet<>(externalIds.byAccount(admin.id));
     externalIdReader.setFailOnLoad(true);
 
     // insert external ID
     ExternalId extId = ExternalId.create("foo", "bar", admin.id);
-    extIdsUpdate.create().insert(db, extId);
+    extIdsUpdate.create().insert(extId);
     expectedExtIds.add(extId);
-    assertThat(externalIds.byAccount(db, admin.id)).containsExactlyElementsIn(expectedExtIds);
+    assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExtIds);
 
     // update external ID
     expectedExtIds.remove(extId);
     extId = ExternalId.createWithEmail("foo", "bar", admin.id, "foo.bar@example.com");
-    extIdsUpdate.create().upsert(db, extId);
+    extIdsUpdate.create().upsert(extId);
     expectedExtIds.add(extId);
-    assertThat(externalIds.byAccount(db, admin.id)).containsExactlyElementsIn(expectedExtIds);
+    assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExtIds);
 
     // delete external ID
-    extIdsUpdate.create().delete(db, extId);
+    extIdsUpdate.create().delete(extId);
     expectedExtIds.remove(extId);
-    assertThat(externalIds.byAccount(db, admin.id)).containsExactlyElementsIn(expectedExtIds);
+    assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExtIds);
   }
 
   @Test
-  @GerritConfig(name = "user.readExternalIdsFromGit", value = "true")
   public void byAccountFailIfReadingExternalIdsFails() throws Exception {
     externalIdReader.setFailOnLoad(true);
 
@@ -609,11 +712,10 @@
     insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id));
 
     exception.expect(IOException.class);
-    externalIds.byAccount(db, admin.id);
+    externalIds.byAccount(admin.id);
   }
 
   @Test
-  @GerritConfig(name = "user.readExternalIdsFromGit", value = "true")
   public void byEmailFailIfReadingExternalIdsFails() throws Exception {
     externalIdReader.setFailOnLoad(true);
 
@@ -621,17 +723,16 @@
     insertExtIdBehindGerritsBack(ExternalId.create("foo", "bar", admin.id));
 
     exception.expect(IOException.class);
-    externalIds.byEmail(db, admin.email);
+    externalIds.byEmail(admin.email);
   }
 
   @Test
-  @GerritConfig(name = "user.readExternalIdsFromGit", value = "true")
   public void byAccountUpdateExternalIdsBehindGerritsBack() throws Exception {
-    Set<ExternalId> expectedExternalIds = new HashSet<>(externalIds.byAccount(db, admin.id));
+    Set<ExternalId> expectedExternalIds = new HashSet<>(externalIds.byAccount(admin.id));
     ExternalId newExtId = ExternalId.create("foo", "bar", admin.id);
     insertExtIdBehindGerritsBack(newExtId);
     expectedExternalIds.add(newExtId);
-    assertThat(externalIds.byAccount(db, admin.id)).containsExactlyElementsIn(expectedExternalIds);
+    assertThat(externalIds.byAccount(admin.id)).containsExactlyElementsIn(expectedExternalIds);
   }
 
   private void insertExtIdBehindGerritsBack(ExternalId extId) throws Exception {
@@ -645,4 +746,54 @@
           repo, rw, ins, rev, noteMap, "insert new ID", serverIdent.get(), serverIdent.get());
     }
   }
+
+  private void addExtId(TestRepository<?> testRepo, ExternalId... extIds)
+      throws IOException, OrmDuplicateKeyException, ConfigInvalidException {
+    ObjectId rev = ExternalIdReader.readRevision(testRepo.getRepository());
+
+    try (ObjectInserter ins = testRepo.getRepository().newObjectInserter()) {
+      NoteMap noteMap = ExternalIdReader.readNoteMap(testRepo.getRevWalk(), rev);
+      for (ExternalId extId : extIds) {
+        ExternalIdsUpdate.insert(testRepo.getRevWalk(), ins, noteMap, extId);
+      }
+
+      ExternalIdsUpdate.commit(
+          testRepo.getRepository(),
+          testRepo.getRevWalk(),
+          ins,
+          rev,
+          noteMap,
+          "Add external ID",
+          admin.getIdent(),
+          admin.getIdent());
+    }
+  }
+
+  private List<AccountExternalIdInfo> toExternalIdInfos(Collection<ExternalId> extIds) {
+    return extIds.stream().map(this::toExternalIdInfo).collect(toList());
+  }
+
+  private AccountExternalIdInfo toExternalIdInfo(ExternalId extId) {
+    AccountExternalIdInfo info = new AccountExternalIdInfo();
+    info.identity = extId.key().get();
+    info.emailAddress = extId.email();
+    info.canDelete = !extId.isScheme(SCHEME_USERNAME) ? true : null;
+    info.trusted =
+        extId.isScheme(SCHEME_MAILTO)
+                || extId.isScheme(SCHEME_UUID)
+                || extId.isScheme(SCHEME_USERNAME)
+            ? true
+            : null;
+    return info;
+  }
+
+  private void allowPushOfExternalIds() throws IOException, ConfigInvalidException {
+    grant(Permission.READ, allUsers, RefNames.REFS_EXTERNAL_IDS);
+    grant(Permission.PUSH, allUsers, RefNames.REFS_EXTERNAL_IDS);
+  }
+
+  private void assertRefUpdateFailure(RemoteRefUpdate update, String msg) {
+    assertThat(update.getStatus()).isEqualTo(Status.REJECTED_OTHER_REASON);
+    assertThat(update.getMessage()).isEqualTo(msg);
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 153e70b..461e6bc 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -16,11 +16,14 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.common.data.Permission.READ;
 import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 import static org.eclipse.jgit.lib.Constants.SIGNED_OFF_BY_TAG;
 
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.GitUtil;
@@ -40,9 +43,11 @@
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.MergeInput;
 import com.google.gerrit.extensions.common.RevisionInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
@@ -52,6 +57,7 @@
 import com.google.gerrit.testutil.TestTimeUtil;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
@@ -170,6 +176,29 @@
   }
 
   @Test
+  public void createChangeWithoutAccessToParentCommitFails() throws Exception {
+    Map<String, PushOneCommit.Result> results =
+        changeInTwoBranches("invisible-branch", "a.txt", "visible-branch", "b.txt");
+    block(READ, REGISTERED_USERS, "refs/heads/invisible-branch", project);
+
+    ChangeInput in = newChangeInput(ChangeStatus.NEW);
+    in.branch = "visible-branch";
+    in.baseChange = results.get("invisible-branch").getChangeId();
+    assertCreateFails(
+        in, UnprocessableEntityException.class, "Base change not found: " + in.baseChange);
+  }
+
+  @Test
+  public void createChangeOnInvisibleBranchFails() throws Exception {
+    changeInTwoBranches("invisible-branch", "a.txt", "branchB", "b.txt");
+    block(READ, REGISTERED_USERS, "refs/heads/invisible-branch", project);
+
+    ChangeInput in = newChangeInput(ChangeStatus.NEW);
+    in.branch = "invisible-branch";
+    assertCreateFails(in, AuthException.class, "cannot upload review");
+  }
+
+  @Test
   public void noteDbCommit() throws Exception {
     assume().that(notesMigration.readChanges()).isTrue();
 
@@ -444,8 +473,18 @@
     return in;
   }
 
-  private void changeInTwoBranches(String branchA, String fileA, String branchB, String fileB)
-      throws Exception {
+  /**
+   * Create an empty commit in master, two new branches with one commit each.
+   *
+   * @param branchA name of first branch to create
+   * @param fileA name of file to commit to branchA
+   * @param branchB name of second branch to create
+   * @param fileB name of file to commit to branchB
+   * @return A {@code Map} of branchName => commit result.
+   * @throws Exception
+   */
+  private Map<String, Result> changeInTwoBranches(
+      String branchA, String fileA, String branchB, String fileB) throws Exception {
     // create a initial commit in master
     Result initialCommit =
         pushFactory
@@ -470,5 +509,7 @@
     commitB.setParent(initialCommit.getCommit());
     Result changeB = commitB.to("refs/heads/" + branchB);
     changeB.assertOkStatus();
+
+    return ImmutableMap.of("master", initialCommit, branchA, changeA, branchB, changeB);
   }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index 04151e9..0ac263f 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -30,9 +30,8 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.InputStream;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -547,10 +546,10 @@
     try (BinaryResult request = submitPreview(change1.getChangeId(), "tgz")) {
       assertThat(request.getContentType()).isEqualTo("application/x-gzip");
       tempfile = File.createTempFile("test", null);
-      request.writeTo(new FileOutputStream(tempfile));
+      request.writeTo(Files.newOutputStream(tempfile.toPath()));
     }
 
-    InputStream is = new GZIPInputStream(new FileInputStream(tempfile));
+    InputStream is = new GZIPInputStream(Files.newInputStream(tempfile.toPath()));
 
     List<String> untarredFiles = new ArrayList<>();
     try (TarArchiveInputStream tarInputStream =
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 26a49b1..16dbee3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -20,12 +20,14 @@
 
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.AcceptanceTestRequestScope;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.extensions.api.changes.DeleteCommentInput;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
@@ -34,27 +36,40 @@
 import com.google.gerrit.extensions.client.Side;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.IdString;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.change.ChangeResource;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.PostReview;
 import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.notedb.ChangeNoteUtil;
+import com.google.gerrit.server.notedb.DeleteCommentRewriter;
 import com.google.gerrit.testutil.FakeEmailSender;
 import com.google.gerrit.testutil.FakeEmailSender.Message;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.function.Supplier;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -67,6 +82,8 @@
 
   @Inject private FakeEmailSender email;
 
+  @Inject private ChangeNoteUtil noteUtil;
+
   private final Integer[] lines = {0, 1};
 
   @Before
@@ -739,6 +756,167 @@
     }
   }
 
+  @Test
+  public void deleteCommentCannotBeAppliedByUser() throws Exception {
+    PushOneCommit.Result result = createChange();
+    CommentInput targetComment = addComment(result.getChangeId(), "My password: abc123");
+
+    Map<String, List<CommentInfo>> commentsMap =
+        getPublishedComments(result.getChangeId(), result.getCommit().name());
+
+    assertThat(commentsMap.size()).isEqualTo(1);
+    assertThat(commentsMap.get(FILE_NAME)).hasSize(1);
+
+    String uuid = commentsMap.get(targetComment.path).get(0).id;
+    DeleteCommentInput input = new DeleteCommentInput("contains confidential information");
+
+    setApiUser(user);
+    exception.expect(AuthException.class);
+    gApi.changes().id(result.getChangeId()).current().comment(uuid).delete(input);
+  }
+
+  @Test
+  public void deleteCommentByRewritingCommitHistory() throws Exception {
+    // Create change (the 1st commit on the change's meta branch).
+    PushOneCommit.Result result = createChange();
+    String changeId = result.getChangeId();
+    Change.Id id = result.getChange().getId();
+
+    // Add two comments in patch set 1 (the 2nd commit on the change's meta branch).
+    ReviewInput reviewInput = new ReviewInput();
+    CommentInput comment1 = newComment(FILE_NAME, Side.REVISION, 0, "My password: abc123", false);
+    CommentInput comment2 = newComment(FILE_NAME, Side.REVISION, 1, "nit: long line", false);
+    reviewInput.comments = ImmutableMap.of(FILE_NAME, Lists.newArrayList(comment1, comment2));
+    reviewInput.label("Code-Review", 1);
+    gApi.changes().id(changeId).current().review(reviewInput);
+
+    // Create patch set 2 (the 3rd commit on the change's meta branch).
+    amendChange(changeId);
+
+    // Add 'comment3' in patch set 2 (the 4th commit on the change's meta branch).
+    CommentInput comment3 = addComment(changeId, "typo");
+
+    Map<String, List<CommentInfo>> commentsMap = getPublishedComments(changeId);
+    assertThat(commentsMap).hasSize(1);
+    assertThat(commentsMap.get(FILE_NAME)).hasSize(3);
+    Optional<CommentInfo> targetCommentInfo =
+        commentsMap
+            .get(FILE_NAME)
+            .stream()
+            .filter(c -> c.message.equals("My password: abc123"))
+            .findFirst();
+    assertThat(targetCommentInfo.isPresent()).isTrue();
+
+    List<RevCommit> commitsBeforeDelete = new ArrayList<>();
+    if (notesMigration.commitChangeWrites()) {
+      commitsBeforeDelete = getCommits(id);
+    }
+
+    String uuid = targetCommentInfo.get().id;
+    // Get the target comment.
+    CommentInfo oldComment =
+        gApi.changes().id(changeId).revision(result.getCommit().getName()).comment(uuid).get();
+
+    // Delete the target comment.
+    DeleteCommentInput input = new DeleteCommentInput("contains confidential information");
+    setApiUser(admin);
+    CommentInfo updatedComment =
+        gApi.changes()
+            .id(changeId)
+            .revision(result.getCommit().getName())
+            .comment(uuid)
+            .delete(input);
+
+    String expectedMsg =
+        String.format("Comment removed by: %s; Reason: %s", admin.fullName, input.reason);
+
+    assertThat(updatedComment.message).isEqualTo(expectedMsg);
+    updatedComment.message = oldComment.message;
+    assertThat(updatedComment).isEqualTo(oldComment);
+
+    // Check the comment's message has been replaced in NoteDb.
+    if (notesMigration.commitChangeWrites()) {
+      assertMetaBranchCommitsAfterRewriting(commitsBeforeDelete, id, uuid, expectedMsg);
+    }
+
+    // Make sure that comments can still be added correctly.
+    CommentInput comment4 = addComment(changeId, "too much space");
+    commentsMap = getPublishedComments(changeId);
+
+    assertThat(commentsMap).hasSize(1);
+    List<CommentInput> comments =
+        Lists.transform(commentsMap.get(FILE_NAME), infoToInput(FILE_NAME));
+
+    // Change comment1's message to the expected message.
+    comment1.message = expectedMsg;
+    assertThat(comments).containsExactly(comment1, comment2, comment3, comment4);
+  }
+
+  private CommentInput addComment(String changeId, String message) throws Exception {
+    ReviewInput input = new ReviewInput();
+    CommentInput comment = newComment(FILE_NAME, Side.REVISION, 0, message, false);
+    input.comments = ImmutableMap.of(comment.path, Lists.newArrayList(comment));
+    gApi.changes().id(changeId).current().review(input);
+    return comment;
+  }
+
+  private List<RevCommit> getCommits(Change.Id changeId) throws IOException {
+    try (Repository repo = repoManager.openRepository(project);
+        RevWalk revWalk = new RevWalk(repo)) {
+      Ref metaRef = repo.exactRef(RefNames.changeMetaRef(changeId));
+      revWalk.markStart(revWalk.parseCommit(metaRef.getObjectId()));
+      return Lists.newArrayList(revWalk);
+    }
+  }
+
+  /**
+   * All the commits, which contain the target comment before, should still contain the comment with
+   * the updated message. All the other metas of the commits should be exactly the same.
+   */
+  private void assertMetaBranchCommitsAfterRewriting(
+      List<RevCommit> beforeDelete,
+      Change.Id changeId,
+      String targetCommentUuid,
+      String expectedMessage)
+      throws Exception {
+    List<RevCommit> afterDelete = getCommits(changeId);
+    assertThat(afterDelete).hasSize(beforeDelete.size());
+
+    try (Repository repo = repoManager.openRepository(project);
+        ObjectReader reader = repo.newObjectReader()) {
+      for (int i = 0; i < beforeDelete.size(); i++) {
+        RevCommit commitBefore = beforeDelete.get(i);
+        RevCommit commitAfter = afterDelete.get(i);
+
+        Map<String, com.google.gerrit.reviewdb.client.Comment> commentMapBefore =
+            DeleteCommentRewriter.getPublishedComments(
+                noteUtil, changeId, reader, NoteMap.read(reader, commitBefore));
+        Map<String, com.google.gerrit.reviewdb.client.Comment> commentMapAfter =
+            DeleteCommentRewriter.getPublishedComments(
+                noteUtil, changeId, reader, NoteMap.read(reader, commitAfter));
+
+        if (commentMapBefore.containsKey(targetCommentUuid)) {
+          assertThat(commentMapAfter).containsKey(targetCommentUuid);
+          com.google.gerrit.reviewdb.client.Comment comment =
+              commentMapAfter.get(targetCommentUuid);
+          assertThat(comment.message).isEqualTo(expectedMessage);
+          comment.message = commentMapBefore.get(targetCommentUuid).message;
+          commentMapAfter.put(targetCommentUuid, comment);
+          assertThat(commentMapAfter).isEqualTo(commentMapBefore);
+        } else {
+          assertThat(commentMapAfter).doesNotContainKey(targetCommentUuid);
+        }
+
+        // Other metas should be exactly the same.
+        assertThat(commitAfter.getFullMessage()).isEqualTo(commitBefore.getFullMessage());
+        assertThat(commitAfter.getCommitterIdent()).isEqualTo(commitBefore.getCommitterIdent());
+        assertThat(commitAfter.getAuthorIdent()).isEqualTo(commitBefore.getAuthorIdent());
+        assertThat(commitAfter.getEncoding()).isEqualTo(commitBefore.getEncoding());
+        assertThat(commitAfter.getEncodingName()).isEqualTo(commitBefore.getEncodingName());
+      }
+    }
+  }
+
   private static String extractComments(String msg) {
     // Extract lines between start "....." and end "-- ".
     Pattern p = Pattern.compile(".*[.]{5}\n+(.*)\\n+-- \n.*", Pattern.DOTALL);
@@ -803,6 +981,10 @@
     return gApi.changes().id(changeId).revision(revId).drafts();
   }
 
+  private Map<String, List<CommentInfo>> getPublishedComments(String changeId) throws Exception {
+    return gApi.changes().id(changeId).comments();
+  }
+
   private CommentInfo getDraftComment(String changeId, String revId, String uuid) throws Exception {
     return gApi.changes().id(changeId).revision(revId).draft(uuid).get();
   }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
index 8d7a452..3dabced 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ChangeApi.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.extensions.api.changes;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -85,7 +86,7 @@
 
   void move(MoveInput in) throws RestApiException;
 
-  void setPrivate(boolean value) throws RestApiException;
+  void setPrivate(boolean value, @Nullable String message) throws RestApiException;
 
   void setWorkInProgress(String message) throws RestApiException;
 
@@ -335,7 +336,7 @@
     }
 
     @Override
-    public void setPrivate(boolean value) {
+    public void setPrivate(boolean value, @Nullable String message) {
       throw new NotImplementedException();
     }
 
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
index 78f2b89..46827e5 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/CommentApi.java
@@ -22,6 +22,17 @@
   CommentInfo get() throws RestApiException;
 
   /**
+   * Deletes a published comment of a revision. For NoteDb, it deletes the comment by rewriting the
+   * commit history.
+   *
+   * <p>Note instead of deleting the whole comment, this endpoint just replaces the comment's
+   * message.
+   *
+   * @return the comment with its message updated.
+   */
+  CommentInfo delete(DeleteCommentInput input) throws RestApiException;
+
+  /**
    * A default implementation which allows source compatibility when adding new methods to the
    * interface.
    */
@@ -30,5 +41,10 @@
     public CommentInfo get() {
       throw new NotImplementedException();
     }
+
+    @Override
+    public CommentInfo delete(DeleteCommentInput input) {
+      throw new NotImplementedException();
+    }
   }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteCommentInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteCommentInput.java
new file mode 100644
index 0000000..75fd16b
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/DeleteCommentInput.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.changes;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+public class DeleteCommentInput {
+  @DefaultInput public String reason;
+
+  public DeleteCommentInput() {
+    reason = "";
+  }
+
+  public DeleteCommentInput(String reason) {
+    this.reason = Strings.nullToEmpty(reason);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GroupIndexedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GroupIndexedListener.java
new file mode 100644
index 0000000..d499020
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GroupIndexedListener.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/** Notified whenever a group is indexed */
+@ExtensionPoint
+public interface GroupIndexedListener {
+  /**
+   * Invoked when a group is indexed
+   *
+   * @param uuid of the group
+   */
+  void onGroupIndexed(String uuid);
+}
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 64286c4..4e6ea66 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
@@ -23,7 +23,6 @@
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.gpg.PublicKeyStore;
 import com.google.gerrit.gpg.server.DeleteGpgKey.Input;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -43,7 +42,6 @@
   public static class Input {}
 
   private final Provider<PersonIdent> serverIdent;
-  private final Provider<ReviewDb> db;
   private final Provider<PublicKeyStore> storeProvider;
   private final AccountCache accountCache;
   private final ExternalIdsUpdate.User externalIdsUpdateFactory;
@@ -51,12 +49,10 @@
   @Inject
   DeleteGpgKey(
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
-      Provider<ReviewDb> db,
       Provider<PublicKeyStore> storeProvider,
       AccountCache accountCache,
       ExternalIdsUpdate.User externalIdsUpdateFactory) {
     this.serverIdent = serverIdent;
-    this.db = db;
     this.storeProvider = storeProvider;
     this.accountCache = accountCache;
     this.externalIdsUpdateFactory = externalIdsUpdateFactory;
@@ -70,7 +66,6 @@
     externalIdsUpdateFactory
         .create()
         .delete(
-            db.get(),
             rsrc.getUser().getAccountId(),
             ExternalId.Key.create(
                 SCHEME_GPGKEY, BaseEncoding.base16().encode(key.getFingerprint())));
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
index 13fb368..678247e 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/server/GpgKeys.java
@@ -34,7 +34,6 @@
 import com.google.gerrit.gpg.GerritPublicKeyChecker;
 import com.google.gerrit.gpg.PublicKeyChecker;
 import com.google.gerrit.gpg.PublicKeyStore;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountResource;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -64,7 +63,6 @@
   public static final String MIME_TYPE = "application/pgp-keys";
 
   private final DynamicMap<RestView<GpgKey>> views;
-  private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> self;
   private final Provider<PublicKeyStore> storeProvider;
   private final GerritPublicKeyChecker.Factory checkerFactory;
@@ -73,13 +71,11 @@
   @Inject
   GpgKeys(
       DynamicMap<RestView<GpgKey>> views,
-      Provider<ReviewDb> db,
       Provider<CurrentUser> self,
       Provider<PublicKeyStore> storeProvider,
       GerritPublicKeyChecker.Factory checkerFactory,
       ExternalIds externalIds) {
     this.views = views;
-    this.db = db;
     this.self = self;
     this.storeProvider = storeProvider;
     this.checkerFactory = checkerFactory;
@@ -199,8 +195,8 @@
     }
   }
 
-  private Iterable<ExternalId> getGpgExtIds(AccountResource rsrc) throws IOException, OrmException {
-    return externalIds.byAccount(db.get(), rsrc.getUser().getAccountId(), SCHEME_GPGKEY);
+  private Iterable<ExternalId> getGpgExtIds(AccountResource rsrc) throws IOException {
+    return externalIds.byAccount(rsrc.getUser().getAccountId(), SCHEME_GPGKEY);
   }
 
   private static long keyId(byte[] fp) {
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 9c04ced..af4d6bb 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
@@ -40,7 +40,6 @@
 import com.google.gerrit.gpg.PublicKeyStore;
 import com.google.gerrit.gpg.server.PostGpgKeys.Input;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -85,7 +84,6 @@
 
   private final Logger log = LoggerFactory.getLogger(getClass());
   private final Provider<PersonIdent> serverIdent;
-  private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> self;
   private final Provider<PublicKeyStore> storeProvider;
   private final GerritPublicKeyChecker.Factory checkerFactory;
@@ -98,7 +96,6 @@
   @Inject
   PostGpgKeys(
       @GerritPersonIdent Provider<PersonIdent> serverIdent,
-      Provider<ReviewDb> db,
       Provider<CurrentUser> self,
       Provider<PublicKeyStore> storeProvider,
       GerritPublicKeyChecker.Factory checkerFactory,
@@ -108,7 +105,6 @@
       ExternalIds externalIds,
       ExternalIdsUpdate.User externalIdsUpdateFactory) {
     this.serverIdent = serverIdent;
-    this.db = db;
     this.self = self;
     this.storeProvider = storeProvider;
     this.checkerFactory = checkerFactory;
@@ -126,7 +122,7 @@
     GpgKeys.checkVisible(self, rsrc);
 
     Collection<ExternalId> existingExtIds =
-        externalIds.byAccount(db.get(), rsrc.getUser().getAccountId(), SCHEME_GPGKEY);
+        externalIds.byAccount(rsrc.getUser().getAccountId(), SCHEME_GPGKEY);
     try (PublicKeyStore store = storeProvider.get()) {
       Set<Fingerprint> toRemove = readKeysToRemove(input, existingExtIds);
       List<PGPPublicKeyRing> newKeys = readKeysToAdd(input, toRemove);
@@ -151,7 +147,7 @@
           toRemove.stream().map(fp -> toExtIdKey(fp.get())).collect(toList());
       externalIdsUpdateFactory
           .create()
-          .replace(db.get(), rsrc.getUser().getAccountId(), extIdKeysToRemove, newExtIds);
+          .replace(rsrc.getUser().getAccountId(), extIdKeysToRemove, newExtIds);
       accountCache.evict(rsrc.getUser().getAccountId());
       return toJson(newKeys, toRemove, store, rsrc.getUser());
     }
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
index d82f95b..deb0dc4 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/GerritPublicKeyCheckerTest.java
@@ -223,7 +223,7 @@
   @Test
   public void noExternalIds() throws Exception {
     ExternalIdsUpdate externalIdsUpdate = externalIdsUpdateFactory.create();
-    externalIdsUpdate.deleteAll(db, user.getAccountId());
+    externalIdsUpdate.deleteAll(user.getAccountId());
     reloadUser();
 
     TestKey key = validKeyWithSecondUserId();
@@ -237,7 +237,7 @@
     assertProblems(
         checker.check(key.getPublicKey()), Status.BAD, "Key is not associated with any users");
     externalIdsUpdate.insert(
-        db, ExternalId.create(toExtIdKey(key.getPublicKey()), user.getAccountId()));
+        ExternalId.create(toExtIdKey(key.getPublicKey()), user.getAccountId()));
     reloadUser();
     assertProblems(checker.check(key.getPublicKey()), Status.BAD, "No identities found for user");
   }
@@ -406,7 +406,7 @@
     cb.setCommitter(ident);
     assertThat(store.save(cb)).isAnyOf(NEW, FAST_FORWARD, FORCED);
 
-    externalIdsUpdateFactory.create().insert(db, newExtIds);
+    externalIdsUpdateFactory.create().insert(newExtIds);
     accountCache.evict(user.getAccountId());
   }
 
@@ -432,7 +432,7 @@
   private void addExternalId(String scheme, String id, String email) throws Exception {
     externalIdsUpdateFactory
         .create()
-        .insert(db, ExternalId.createWithEmail(scheme, id, user.getAccountId(), email));
+        .insert(ExternalId.createWithEmail(scheme, id, user.getAccountId(), email));
     reloadUser();
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
index 74668c1..234df60 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/api/DefaultActions.java
@@ -41,6 +41,11 @@
       @Override
       public void onSuccess(JavaScriptObject in) {
         UiResult result = asUiResult(in);
+        if (result == null) {
+          Gerrit.display(target);
+          return;
+        }
+
         if (result.alert() != null) {
           Window.alert(result.alert());
         }
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 f985f31..915867d 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
@@ -122,11 +122,11 @@
   }
 
   public static void markPrivate(int id, AsyncCallback<JavaScriptObject> cb) {
-    change(id).view("private").put(cb);
+    change(id).view("private").post(PrivateInput.create(), cb);
   }
 
   public static void unmarkPrivate(int id, AsyncCallback<JavaScriptObject> cb) {
-    change(id).view("private").delete(cb);
+    change(id).view("private.delete").post(PrivateInput.create(), cb);
   }
 
   public static RestApi comments(int id) {
@@ -327,6 +327,16 @@
     protected CherryPickInput() {}
   }
 
+  private static class PrivateInput extends JavaScriptObject {
+    static PrivateInput create() {
+      return (PrivateInput) createObject();
+    }
+
+    final native void setMessage(String m) /*-{ this.message = m; }-*/;
+
+    protected PrivateInput() {}
+  }
+
   private static class RebaseInput extends JavaScriptObject {
     final native void setBase(String b) /*-{ this.base = b; }-*/;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
index 651d718..90aedbe 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/RecompileGwtUiFilter.java
@@ -16,9 +16,10 @@
 
 import com.google.gwtexpui.linker.server.UserAgentRule;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Enumeration;
 import java.util.HashSet;
@@ -102,7 +103,7 @@
         mkdir(rawtmp.getParentFile());
         rawtmp.deleteOnExit();
 
-        try (FileOutputStream rawout = new FileOutputStream(rawtmp);
+        try (OutputStream rawout = Files.newOutputStream(rawtmp.toPath());
             InputStream in = zf.getInputStream(ze)) {
           final byte[] buf = new byte[4096];
           int n;
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index a7af056..b22ba49 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -20,9 +20,9 @@
 
 import java.io.File;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
@@ -293,7 +293,7 @@
   private static void extractJar(ZipFile zf, ZipEntry ze, SortedMap<String, URL> jars)
       throws IOException {
     File tmp = createTempFile(safeName(ze), ".jar");
-    try (FileOutputStream out = new FileOutputStream(tmp);
+    try (OutputStream out = Files.newOutputStream(tmp.toPath());
         InputStream in = zf.getInputStream(ze)) {
       byte[] buf = new byte[4096];
       int n;
@@ -414,7 +414,7 @@
     if (src != null) {
       try (InputStream in = src.getLocation().openStream()) {
         final File tmp = createTempFile("gerrit_", ".zip");
-        try (FileOutputStream out = new FileOutputStream(tmp)) {
+        try (OutputStream out = Files.newOutputStream(tmp.toPath())) {
           final byte[] buf = new byte[4096];
           int n;
           while ((n = in.read(buf, 0, buf.length)) > 0) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
index 4e18ddc..3da3596 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
@@ -19,13 +19,11 @@
 
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.externalids.DisabledExternalIdCache;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.account.externalids.ExternalIdsBatchUpdate;
 import com.google.gerrit.server.schema.SchemaVersionCheck;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -38,8 +36,6 @@
   private final LifecycleManager manager = new LifecycleManager();
   private final TextProgressMonitor monitor = new TextProgressMonitor();
 
-  @Inject private SchemaFactory<ReviewDb> database;
-
   @Inject private ExternalIds externalIds;
 
   @Inject private ExternalIdsBatchUpdate externalIdsBatchUpdate;
@@ -63,19 +59,17 @@
             })
         .injectMembers(this);
 
-    try (ReviewDb db = database.open()) {
-      Collection<ExternalId> todo = externalIds.all(db);
-      monitor.beginTask("Converting local usernames", todo.size());
+    Collection<ExternalId> todo = externalIds.all();
+    monitor.beginTask("Converting local usernames", todo.size());
 
-      for (ExternalId extId : todo) {
-        convertLocalUserToLowerCase(extId);
-        monitor.update(1);
-      }
-
-      externalIdsBatchUpdate.commit(db, "Convert local usernames to lower case");
-      monitor.endTask();
-      manager.stop();
+    for (ExternalId extId : todo) {
+      convertLocalUserToLowerCase(extId);
+      monitor.update(1);
     }
+
+    externalIdsBatchUpdate.commit("Convert local usernames to lower case");
+    monitor.endTask();
+    manager.stop();
     return 0;
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java
index fb524a3..d970856 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtobufImport.java
@@ -39,10 +39,10 @@
 import com.google.protobuf.UnknownFieldSet;
 import java.io.BufferedInputStream;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.InputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.nio.file.Files;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -96,7 +96,7 @@
       }
 
       Parser<UnknownFieldSet> parser = UnknownFieldSet.getDefaultInstance().getParserForType();
-      try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
+      try (InputStream in = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
         UnknownFieldSet msg;
         while ((msg = parser.parseDelimitedFrom(in)) != null) {
           Map.Entry<Integer, UnknownFieldSet.Field> e =
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 38c3321..c7606d6 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -277,6 +277,7 @@
       }
       c.setInheritChannel(cfg.getBoolean("httpd", "inheritChannel", false));
       c.setReuseAddress(reuseAddress);
+      c.setIdleTimeout(cfg.getTimeUnit("httpd", null, "idleTimeout", 30000L, MILLISECONDS));
       connectors[idx] = c;
     }
     return connectors;
@@ -398,12 +399,12 @@
     //
     app.setContextPath(contextPath);
 
-    // HTTP front-end filter to be used as surrogate of Apache HTTP
+    // HTTP front-end filters to be used as surrogate of Apache HTTP
     // reverse-proxy filtering.
     // It is meant to be used as simpler tiny deployment of custom-made
     // security enforcement (Security tokens, IP-based security filtering, others)
-    String filterClassName = cfg.getString("httpd", null, "filterClass");
-    if (filterClassName != null) {
+    String[] filterClassNames = cfg.getStringList("httpd", null, "filterClass");
+    for (String filterClassName : filterClassNames) {
       try {
         @SuppressWarnings("unchecked")
         Class<? extends Filter> filterClass =
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
index 5bedb1b..9906089 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ExternalIdsOnInit.java
@@ -14,10 +14,7 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.server.account.externalids.ExternalId.toAccountExternalIds;
-
 import com.google.gerrit.pgm.init.api.InitFlags;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIdReader;
@@ -52,9 +49,8 @@
     this.allUsers = allUsers.get();
   }
 
-  public synchronized void insert(ReviewDb db, String commitMessage, Collection<ExternalId> extIds)
+  public synchronized void insert(String commitMessage, Collection<ExternalId> extIds)
       throws OrmException, IOException, ConfigInvalidException {
-    db.accountExternalIds().insert(toAccountExternalIds(extIds));
 
     File path = getPath();
     if (path != null) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
index 1e6bfa8..466404b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -104,7 +104,7 @@
           if (email != null) {
             extIds.add(ExternalId.createEmail(id, email));
           }
-          externalIds.insert(db, "Add external IDs for initial admin user", extIds);
+          externalIds.insert("Add external IDs for initial admin user", extIds);
 
           Account a = new Account(id, TimeUtil.nowTs());
           a.setFullName(name);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
index a563a55..361e4b6 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.common.FileUtil.chmod;
-import static com.google.gerrit.pgm.init.api.InitUtil.die;
 import static com.google.gerrit.pgm.init.api.InitUtil.hostname;
 import static java.nio.file.Files.exists;
 
@@ -30,10 +28,6 @@
 import java.io.IOException;
 import java.lang.ProcessBuilder.Redirect;
 import java.net.InetSocketAddress;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import org.apache.sshd.common.util.security.SecurityUtils;
-import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
 
 /** Initialize the {@code sshd} configuration section. */
 @Singleton
@@ -86,20 +80,21 @@
   }
 
   private void generateSshHostKeys() throws InterruptedException, IOException {
-    if (!exists(site.ssh_key) //
-        && !exists(site.ssh_rsa) //
-        && !exists(site.ssh_dsa)) {
+    if (!exists(site.ssh_key)
+        && (!exists(site.ssh_rsa)
+            || !exists(site.ssh_dsa)
+            || !exists(site.ssh_ed25519)
+            || !exists(site.ssh_ecdsa))) {
       System.err.print("Generating SSH host key ...");
       System.err.flush();
 
-      if (SecurityUtils.isBouncyCastleRegistered()) {
-        // Generate the SSH daemon host key using ssh-keygen.
-        //
-        final String comment = "gerrit-code-review@" + hostname();
+      // Generate the SSH daemon host key using ssh-keygen.
+      //
+      final String comment = "gerrit-code-review@" + hostname();
 
-        // Workaround for JDK-6518827 - zero-length argument ignored on Win32
-        String emptyPassphraseArg = HostPlatform.isWin32() ? "\"\"" : "";
-
+      // Workaround for JDK-6518827 - zero-length argument ignored on Win32
+      String emptyPassphraseArg = HostPlatform.isWin32() ? "\"\"" : "";
+      if (!exists(site.ssh_rsa)) {
         System.err.print(" rsa...");
         System.err.flush();
         new ProcessBuilder(
@@ -117,7 +112,9 @@
             .redirectOutput(Redirect.INHERIT)
             .start()
             .waitFor();
+      }
 
+      if (!exists(site.ssh_dsa)) {
         System.err.print(" dsa...");
         System.err.flush();
         new ProcessBuilder(
@@ -135,42 +132,57 @@
             .redirectOutput(Redirect.INHERIT)
             .start()
             .waitFor();
+      }
 
-      } else {
-        // Generate the SSH daemon host key ourselves. This is complex
-        // because SimpleGeneratorHostKeyProvider doesn't mark the data
-        // file as only readable by us, exposing the private key for a
-        // short period of time. We try to reduce that risk by creating
-        // the key within a temporary directory.
-        //
-        Path tmpdir = site.etc_dir.resolve("tmp.sshkeygen");
-        try {
-          Files.createDirectory(tmpdir);
-        } catch (IOException e) {
-          throw die("Cannot create directory " + tmpdir, e);
-        }
-        chmod(0600, tmpdir);
-
-        Path tmpkey = tmpdir.resolve(site.ssh_key.getFileName().toString());
-        SimpleGeneratorHostKeyProvider p;
-
-        System.err.print(" rsa(simple)...");
+      if (!exists(site.ssh_ed25519)) {
+        System.err.print(" ed25519...");
         System.err.flush();
-        p = new SimpleGeneratorHostKeyProvider();
-        p.setPath(tmpkey.toAbsolutePath());
-        p.setAlgorithm("RSA");
-        p.loadKeys(); // forces the key to generate.
-        chmod(0600, tmpkey);
-
         try {
-          Files.move(tmpkey, site.ssh_key);
-        } catch (IOException e) {
-          throw die("Cannot rename " + tmpkey + " to " + site.ssh_key, e);
+          new ProcessBuilder(
+                  "ssh-keygen",
+                  "-q" /* quiet */,
+                  "-t",
+                  "ed25519",
+                  "-P",
+                  emptyPassphraseArg,
+                  "-C",
+                  comment,
+                  "-f",
+                  site.ssh_ed25519.toAbsolutePath().toString())
+              .redirectError(Redirect.INHERIT)
+              .redirectOutput(Redirect.INHERIT)
+              .start()
+              .waitFor();
+        } catch (Exception e) {
+          // continue since older hosts won't be able to generate ed25519 keys.
+          System.err.print(" Failed to generate ed25519 key, continuing...");
+          System.err.flush();
         }
+      }
+
+      if (!exists(site.ssh_ecdsa)) {
+        System.err.print(" ecdsa...");
+        System.err.flush();
         try {
-          Files.delete(tmpdir);
-        } catch (IOException e) {
-          throw die("Cannot delete " + tmpdir, e);
+          new ProcessBuilder(
+                  "ssh-keygen",
+                  "-q" /* quiet */,
+                  "-t",
+                  "ecdsa",
+                  "-P",
+                  emptyPassphraseArg,
+                  "-C",
+                  comment,
+                  "-f",
+                  site.ssh_ecdsa.toAbsolutePath().toString())
+              .redirectError(Redirect.INHERIT)
+              .redirectOutput(Redirect.INHERIT)
+              .start()
+              .waitFor();
+        } catch (Exception e) {
+          // continue since older hosts won't be able to generate ecdsa keys.
+          System.err.print(" Failed to generate ecdsa key, continuing...");
+          System.err.flush();
         }
       }
       System.err.println(" done");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/StaleLibraryRemover.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/StaleLibraryRemover.java
index b713067..c454cce 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/StaleLibraryRemover.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/StaleLibraryRemover.java
@@ -49,9 +49,14 @@
         for (Path p : paths) {
           String old = p.getFileName().toString();
           String bak = "." + old + ".backup";
+          Path dest = p.resolveSibling(bak);
+          if (Files.exists(dest)) {
+            ui.message("WARNING: not renaming %s to %s: already exists\n", old, bak);
+            continue;
+          }
           ui.message("Renaming %s to %s\n", old, bak);
           try {
-            Files.move(p, p.resolveSibling(bak));
+            Files.move(p, dest);
           } catch (IOException e) {
             throw new Die("cannot rename " + old, e);
           }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java
index 0ea0f1a..4ad7701 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/rules/PrologCompiler.java
@@ -24,10 +24,9 @@
 import com.googlecode.prolog_cafe.compiler.Compiler;
 import com.googlecode.prolog_cafe.exceptions.CompileException;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URL;
 import java.net.URLClassLoader;
@@ -134,7 +133,7 @@
     // Any leak of tmp caused by this method failing will be cleaned
     // up by our caller when tempDir is recursively deleted.
     File tmp = File.createTempFile("rules", ".pl", tempDir);
-    try (FileOutputStream out = new FileOutputStream(tmp)) {
+    try (OutputStream out = Files.newOutputStream(tmp.toPath())) {
       git.open(blobId).copyTo(out);
     }
     return tmp;
@@ -230,7 +229,7 @@
         jarAdd.setTime(now);
         out.putNextEntry(jarAdd);
         if (f.isFile()) {
-          try (FileInputStream in = new FileInputStream(f)) {
+          try (InputStream in = Files.newInputStream(f.toPath())) {
             while (true) {
               int nRead = in.read(buffer, 0, buffer.length);
               if (nRead <= 0) {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
deleted file mode 100644
index 3c8f2fa..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
+++ /dev/null
@@ -1,186 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.client;
-
-import com.google.gerrit.extensions.client.AuthType;
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.StringKey;
-import java.util.Objects;
-
-/** Association of an external account identifier to a local {@link Account}. */
-public final class AccountExternalId {
-  /**
-   * Scheme used for {@link AuthType#LDAP}, {@link AuthType#CLIENT_SSL_CERT_LDAP}, {@link
-   * AuthType#HTTP_LDAP}, and {@link AuthType#LDAP_BIND} usernames.
-   *
-   * <p>The name {@code gerrit:} was a very poor choice.
-   */
-  public static final String SCHEME_GERRIT = "gerrit:";
-
-  /** Scheme used for randomly created identities constructed by a UUID. */
-  public static final String SCHEME_UUID = "uuid:";
-
-  /** Scheme used to represent only an email address. */
-  public static final String SCHEME_MAILTO = "mailto:";
-
-  /** Scheme for the username used to authenticate an account, e.g. over SSH. */
-  public static final String SCHEME_USERNAME = "username:";
-
-  /** Scheme used for GPG public keys. */
-  public static final String SCHEME_GPGKEY = "gpgkey:";
-
-  /** Scheme for external auth used during authentication, e.g. OAuth Token */
-  public static final String SCHEME_EXTERNAL = "external:";
-
-  public static class Key extends StringKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected String externalId;
-
-    protected Key() {}
-
-    public Key(String scheme, final String identity) {
-      if (!scheme.endsWith(":")) {
-        scheme += ":";
-      }
-      externalId = scheme + identity;
-    }
-
-    public Key(final String e) {
-      externalId = e;
-    }
-
-    @Override
-    public String get() {
-      return externalId;
-    }
-
-    @Override
-    protected void set(String newValue) {
-      externalId = newValue;
-    }
-
-    public String getScheme() {
-      int c = externalId.indexOf(':');
-      return 0 < c ? externalId.substring(0, c) : null;
-    }
-  }
-
-  @Column(id = 1, name = Column.NONE)
-  protected Key key;
-
-  @Column(id = 2)
-  protected Account.Id accountId;
-
-  @Column(id = 3, notNull = false)
-  protected String emailAddress;
-
-  // Encoded version of the hashed and salted password, to be interpreted by the
-  // {@link HashedPassword} class.
-  @Column(id = 4, notNull = false)
-  protected String password;
-
-  /** <i>computed value</i> is this identity trusted by the site administrator? */
-  protected boolean trusted;
-
-  /** <i>computed value</i> can this identity be removed from the account? */
-  protected boolean canDelete;
-
-  protected AccountExternalId() {}
-
-  /**
-   * Create a new binding to an external identity.
-   *
-   * @param who the account this binds to.
-   * @param k the binding key.
-   */
-  public AccountExternalId(final Account.Id who, final AccountExternalId.Key k) {
-    accountId = who;
-    key = k;
-  }
-
-  public AccountExternalId.Key getKey() {
-    return key;
-  }
-
-  /** Get local id of this account, to link with in other entities */
-  public Account.Id getAccountId() {
-    return accountId;
-  }
-
-  public String getExternalId() {
-    return key.externalId;
-  }
-
-  public String getEmailAddress() {
-    return emailAddress;
-  }
-
-  public void setEmailAddress(final String e) {
-    emailAddress = e;
-  }
-
-  public boolean isScheme(final String scheme) {
-    final String id = getExternalId();
-    return id != null && id.startsWith(scheme);
-  }
-
-  public String getSchemeRest() {
-    String scheme = key.getScheme();
-    return null != scheme ? getExternalId().substring(scheme.length() + 1) : null;
-  }
-
-  public void setPassword(String hashed) {
-    password = hashed;
-  }
-
-  public String getPassword() {
-    return password;
-  }
-
-  public boolean isTrusted() {
-    return trusted;
-  }
-
-  public void setTrusted(final boolean t) {
-    trusted = t;
-  }
-
-  public boolean canDelete() {
-    return canDelete;
-  }
-
-  public void setCanDelete(final boolean t) {
-    canDelete = t;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o instanceof AccountExternalId) {
-      AccountExternalId extId = (AccountExternalId) o;
-      return Objects.equals(key, extId.key)
-          && Objects.equals(accountId, extId.accountId)
-          && Objects.equals(emailAddress, extId.emailAddress)
-          && Objects.equals(password, extId.password);
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    return Objects.hash(key, accountId, emailAddress, password);
-  }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
deleted file mode 100644
index e21faaf..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (C) 2008 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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountExternalIdAccess extends Access<AccountExternalId, AccountExternalId.Key> {
-  @Override
-  @PrimaryKey("key")
-  AccountExternalId get(AccountExternalId.Key key) throws OrmException;
-
-  @Query("WHERE accountId = ?")
-  ResultSet<AccountExternalId> byAccount(Account.Id id) throws OrmException;
-
-  @Query("WHERE emailAddress = ?")
-  ResultSet<AccountExternalId> byEmailAddress(String email) throws OrmException;
-
-  @Query
-  ResultSet<AccountExternalId> all() throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 49b9337..165578a 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -50,8 +50,7 @@
   @Relation(id = 6)
   AccountAccess accounts();
 
-  @Relation(id = 7)
-  AccountExternalIdAccess accountExternalIds();
+  // Deleted @Relation(id = 7)
 
   // Deleted @Relation(id = 8)
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
index 8aeb3ad..3b22889 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDbWrapper.java
@@ -83,11 +83,6 @@
   }
 
   @Override
-  public AccountExternalIdAccess accountExternalIds() {
-    return delegate.accountExternalIds();
-  }
-
-  @Override
   public AccountGroupAccess accountGroups() {
     return delegate.accountGroups();
   }
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index 2fcf53c..871ed20 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -16,16 +16,6 @@
 
 
 -- *********************************************************************
--- AccountExternalIdAccess
---    covers:             byAccount
-CREATE INDEX account_external_ids_byAccount
-ON account_external_ids (account_id);
-
---    covers:             byEmailAddress
-CREATE INDEX account_external_ids_byEmail
-ON account_external_ids (email_address);
-
--- *********************************************************************
 -- AccountGroupMemberAccess
 --    @PrimaryKey covers: byAccount
 CREATE INDEX account_group_members_byGroup
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
index 3be3c26..c349241 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_maxdb.sql
@@ -19,18 +19,6 @@
 
 
 -- *********************************************************************
--- AccountExternalIdAccess
---    covers:             byAccount
-CREATE INDEX account_external_ids_byAccount
-ON account_external_ids (account_id)
-#
-
---    covers:             byEmailAddress
-CREATE INDEX account_external_ids_byEmail
-ON account_external_ids (email_address)
-#
-
--- *********************************************************************
 -- AccountGroupMemberAccess
 --    @PrimaryKey covers: byAccount
 CREATE INDEX account_group_members_byGroup
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index 641c613..da99fef 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -63,17 +63,6 @@
 
 
 -- *********************************************************************
--- AccountExternalIdAccess
---    covers:             byAccount
-CREATE INDEX account_external_ids_byAccount
-ON account_external_ids (account_id);
-
---    covers:             byEmailAddress
-CREATE INDEX account_external_ids_byEmail
-ON account_external_ids (email_address);
-
-
--- *********************************************************************
 -- AccountGroupMemberAccess
 --    @PrimaryKey covers: byAccount
 CREATE INDEX account_group_members_byGroup
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
index 6c342c1..249ec7e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CommentsUtil.java
@@ -16,6 +16,7 @@
 
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.reviewdb.client.PatchLineComment.Status.PUBLISHED;
 import static java.util.stream.Collectors.toList;
 import static java.util.stream.Collectors.toSet;
 
@@ -44,6 +45,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -223,8 +225,7 @@
 
   public List<Comment> publishedByChange(ReviewDb db, ChangeNotes notes) throws OrmException {
     if (!migration.readChanges()) {
-      return sort(
-          byCommentStatus(db.patchComments().byChange(notes.getChangeId()), Status.PUBLISHED));
+      return sort(byCommentStatus(db.patchComments().byChange(notes.getChangeId()), PUBLISHED));
     }
 
     notes.load();
@@ -403,6 +404,25 @@
         .delete(toPatchLineComments(update.getId(), PatchLineComment.Status.DRAFT, comments));
   }
 
+  public void deleteCommentByRewritingHistory(
+      ReviewDb db, ChangeUpdate update, Comment.Key commentKey, PatchSet.Id psId, String newMessage)
+      throws OrmException {
+    if (PrimaryStorage.of(update.getChange()).equals(PrimaryStorage.REVIEW_DB)) {
+      PatchLineComment.Key key =
+          new PatchLineComment.Key(new Patch.Key(psId, commentKey.filename), commentKey.uuid);
+      PatchLineComment patchLineComment = db.patchComments().get(key);
+
+      if (!patchLineComment.getStatus().equals(PUBLISHED)) {
+        throw new OrmException(String.format("comment %s is not published", key));
+      }
+
+      patchLineComment.setMessage(newMessage);
+      db.patchComments().upsert(Collections.singleton(patchLineComment));
+    }
+
+    update.deleteCommentByRewritingHistory(commentKey.uuid, newMessage);
+  }
+
   public void deleteAllDraftsFromAllUsers(Change.Id changeId) throws IOException {
     try (Repository repo = repoManager.openRepository(allUsers);
         RevWalk rw = new RevWalk(repo)) {
@@ -533,7 +553,7 @@
       ctx.getUser().updateRealAccountId(d::setRealAuthor);
       setCommentRevId(d, patchListCache, notes.getChange(), ps);
     }
-    putComments(ctx.getDb(), ctx.getUpdate(psId), PatchLineComment.Status.PUBLISHED, drafts);
+    putComments(ctx.getDb(), ctx.getUpdate(psId), PUBLISHED, drafts);
   }
 
   private static PatchSet.Id psId(ChangeNotes notes, Comment c) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
index 08f879f..410dc5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReviewersUtil.java
@@ -101,7 +101,7 @@
     }
   }
 
-  // Generate a candidate list at 3x the size of what the user wants to see to
+  // Generate a candidate list at 2x the size of what the user wants to see to
   // give the ranking algorithm a good set of candidates it can work with
   private static final int CANDIDATE_LIST_MULTIPLIER = 2;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
index 950eac7..0120aed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
@@ -94,7 +94,7 @@
       try (ReviewDb db = schema.open()) {
         return Streams.concat(
                 Streams.stream(db.accounts().byPreferredEmail(email)).map(a -> a.getId()),
-                externalIds.get().byEmail(db, email).stream().map(e -> e.accountId()))
+                externalIds.get().byEmail(email).stream().map(e -> e.accountId()))
             .collect(toImmutableSet());
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index b2f1bae..866a423 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -208,7 +208,7 @@
       return new AccountState(
           account,
           internalGroups,
-          externalIds.byAccount(db, who),
+          externalIds.byAccount(who),
           watchConfig.get().getProjectWatches(who));
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 012ed5c..49a20fa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -171,9 +171,7 @@
       externalIdsUpdateFactory
           .create()
           .replace(
-              db,
-              extId,
-              ExternalId.create(extId.key(), extId.accountId(), newEmail, extId.password()));
+              extId, ExternalId.create(extId.key(), extId.accountId(), newEmail, extId.password()));
     }
 
     if (!realm.allowsEdit(AccountFieldName.FULL_NAME)
@@ -235,7 +233,7 @@
       AccountsUpdate accountsUpdate = accountsUpdateFactory.create();
       accountsUpdate.upsert(db, account);
 
-      ExternalId existingExtId = externalIds.get(db, extId.key());
+      ExternalId existingExtId = externalIds.get(extId.key());
       if (existingExtId != null && !existingExtId.accountId().equals(extId.accountId())) {
         // external ID is assigned to another account, do not overwrite
         accountsUpdate.delete(db, account);
@@ -246,7 +244,7 @@
                 + newId
                 + "; external ID already in use.");
       }
-      externalIdsUpdateFactory.create().upsert(db, extId);
+      externalIdsUpdateFactory.create().upsert(extId);
     } finally {
       // If adding the account failed, it may be that it actually was the
       // first account. So we reset the 'check for first account'-guard, as
@@ -279,7 +277,7 @@
       //
       IdentifiedUser user = userFactory.create(newId);
       try {
-        changeUserNameFactory.create(db, user, who.getUserName()).call();
+        changeUserNameFactory.create(user, who.getUserName()).call();
       } catch (NameAlreadyUsedException e) {
         String message =
             "Cannot assign user name \""
@@ -347,7 +345,7 @@
       // this is why the best we can do here is to fail early and cleanup
       // the database
       accountsUpdateFactory.create().delete(db, account);
-      externalIdsUpdateFactory.create().delete(db, extId);
+      externalIdsUpdateFactory.create().delete(extId);
       throw new AccountUserNameException(errorMessage, e);
     }
   }
@@ -373,8 +371,7 @@
       } else {
         externalIdsUpdateFactory
             .create()
-            .insert(
-                db, ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress()));
+            .insert(ExternalId.createWithEmail(who.getExternalIdKey(), to, who.getEmailAddress()));
 
         if (who.getEmailAddress() != null) {
           Account a = db.accounts().get(to);
@@ -409,22 +406,20 @@
    */
   public AuthResult updateLink(Account.Id to, AuthRequest who)
       throws OrmException, AccountException, IOException, ConfigInvalidException {
-    try (ReviewDb db = schema.open()) {
-      Collection<ExternalId> filteredExtIdsByScheme =
-          externalIds.byAccount(db, to, who.getExternalIdKey().scheme());
+    Collection<ExternalId> filteredExtIdsByScheme =
+        externalIds.byAccount(to, who.getExternalIdKey().scheme());
 
-      if (!filteredExtIdsByScheme.isEmpty()
-          && (filteredExtIdsByScheme.size() > 1
-              || !filteredExtIdsByScheme
-                  .stream()
-                  .filter(e -> e.key().equals(who.getExternalIdKey()))
-                  .findAny()
-                  .isPresent())) {
-        externalIdsUpdateFactory.create().delete(db, filteredExtIdsByScheme);
-      }
-      byIdCache.evict(to);
-      return link(to, who);
+    if (!filteredExtIdsByScheme.isEmpty()
+        && (filteredExtIdsByScheme.size() > 1
+            || !filteredExtIdsByScheme
+                .stream()
+                .filter(e -> e.key().equals(who.getExternalIdKey()))
+                .findAny()
+                .isPresent())) {
+      externalIdsUpdateFactory.create().delete(filteredExtIdsByScheme);
     }
+    byIdCache.evict(to);
+    return link(to, who);
   }
 
   /**
@@ -445,7 +440,7 @@
           throw new AccountException(
               "Identity '" + who.getExternalIdKey().get() + "' in use by another account");
         }
-        externalIdsUpdateFactory.create().delete(db, extId);
+        externalIdsUpdateFactory.create().delete(extId);
 
         if (who.getEmailAddress() != null) {
           Account a = db.accounts().get(from);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
index 1a02ea1..d2a9610 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
@@ -44,7 +43,7 @@
 
   /** Generic factory to change any user's username. */
   public interface Factory {
-    ChangeUserName create(ReviewDb db, IdentifiedUser user, String newUsername);
+    ChangeUserName create(IdentifiedUser user, String newUsername);
   }
 
   private final AccountCache accountCache;
@@ -52,7 +51,6 @@
   private final ExternalIds externalIds;
   private final ExternalIdsUpdate.Server externalIdsUpdateFactory;
 
-  private final ReviewDb db;
   private final IdentifiedUser user;
   private final String newUsername;
 
@@ -62,14 +60,12 @@
       SshKeyCache sshKeyCache,
       ExternalIds externalIds,
       ExternalIdsUpdate.Server externalIdsUpdateFactory,
-      @Assisted ReviewDb db,
       @Assisted IdentifiedUser user,
       @Nullable @Assisted String newUsername) {
     this.accountCache = accountCache;
     this.sshKeyCache = sshKeyCache;
     this.externalIds = externalIds;
     this.externalIdsUpdateFactory = externalIdsUpdateFactory;
-    this.db = db;
     this.user = user;
     this.newUsername = newUsername;
   }
@@ -78,7 +74,7 @@
   public VoidResult call()
       throws OrmException, NameAlreadyUsedException, InvalidUserNameException, IOException,
           ConfigInvalidException {
-    Collection<ExternalId> old = externalIds.byAccount(db, user.getAccountId(), SCHEME_USERNAME);
+    Collection<ExternalId> old = externalIds.byAccount(user.getAccountId(), SCHEME_USERNAME);
     if (!old.isEmpty()) {
       throw new IllegalStateException(USERNAME_CANNOT_BE_CHANGED);
     }
@@ -97,11 +93,11 @@
             password = i.password();
           }
         }
-        externalIdsUpdate.insert(db, ExternalId.create(key, user.getAccountId(), null, password));
+        externalIdsUpdate.insert(ExternalId.create(key, user.getAccountId(), null, password));
       } catch (OrmDuplicateKeyException dupeErr) {
         // If we are using this identity, don't report the exception.
         //
-        ExternalId other = externalIds.get(db, key);
+        ExternalId other = externalIds.get(key);
         if (other != null && other.accountId().equals(user.getAccountId())) {
           return VoidResult.INSTANCE;
         }
@@ -114,7 +110,7 @@
 
     // If we have any older user names, remove them.
     //
-    externalIdsUpdate.delete(db, old);
+    externalIdsUpdate.delete(old);
     for (ExternalId extId : old) {
       sshKeyCache.evict(extId.key().id());
       accountCache.evictByUsername(extId.key().id());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
index 2cfd716..5ea5e96 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -137,11 +137,11 @@
     Account.Id id = new Account.Id(db.nextAccountId());
 
     ExternalId extUser = ExternalId.createUsername(username, id, input.httpPassword);
-    if (externalIds.get(db, extUser.key()) != null) {
+    if (externalIds.get(extUser.key()) != null) {
       throw new ResourceConflictException("username '" + username + "' already exists");
     }
     if (input.email != null) {
-      if (externalIds.get(db, ExternalId.Key.create(SCHEME_MAILTO, input.email)) != null) {
+      if (externalIds.get(ExternalId.Key.create(SCHEME_MAILTO, input.email)) != null) {
         throw new UnprocessableEntityException("email '" + input.email + "' already exists");
       }
       if (!validator.isValid(input.email)) {
@@ -157,17 +157,17 @@
 
     ExternalIdsUpdate externalIdsUpdate = externalIdsUpdateFactory.create();
     try {
-      externalIdsUpdate.insert(db, extIds);
+      externalIdsUpdate.insert(extIds);
     } catch (OrmDuplicateKeyException duplicateKey) {
       throw new ResourceConflictException("username '" + username + "' already exists");
     }
 
     if (input.email != null) {
       try {
-        externalIdsUpdate.insert(db, ExternalId.createEmail(id, input.email));
+        externalIdsUpdate.insert(ExternalId.createEmail(id, input.email));
       } catch (OrmDuplicateKeyException duplicateKey) {
         try {
-          externalIdsUpdate.delete(db, extUser);
+          externalIdsUpdate.delete(extUser);
         } catch (IOException | ConfigInvalidException cleanupError) {
           // Ignored
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
index b4e2bdb..ca56eb1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteEmail.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.DeleteEmail.Input;
@@ -47,7 +46,6 @@
   private final Provider<CurrentUser> self;
   private final Realm realm;
   private final PermissionBackend permissionBackend;
-  private final Provider<ReviewDb> dbProvider;
   private final AccountManager accountManager;
   private final ExternalIds externalIds;
 
@@ -56,13 +54,11 @@
       Provider<CurrentUser> self,
       Realm realm,
       PermissionBackend permissionBackend,
-      Provider<ReviewDb> dbProvider,
       AccountManager accountManager,
       ExternalIds externalIds) {
     this.self = self;
     this.realm = realm;
     this.permissionBackend = permissionBackend;
-    this.dbProvider = dbProvider;
     this.accountManager = accountManager;
     this.externalIds = externalIds;
   }
@@ -87,7 +83,7 @@
 
     Set<ExternalId> extIds =
         externalIds
-            .byAccount(dbProvider.get(), user.getAccountId())
+            .byAccount(user.getAccountId())
             .stream()
             .filter(e -> email.equals(e.email()))
             .collect(toSet());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteExternalIds.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteExternalIds.java
index 27a0038..78eb8a7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteExternalIds.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteExternalIds.java
@@ -24,7 +24,6 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
@@ -41,18 +40,13 @@
   private final AccountManager accountManager;
   private final ExternalIds externalIds;
   private final Provider<CurrentUser> self;
-  private final Provider<ReviewDb> dbProvider;
 
   @Inject
   DeleteExternalIds(
-      AccountManager accountManager,
-      ExternalIds externalIds,
-      Provider<CurrentUser> self,
-      Provider<ReviewDb> dbProvider) {
+      AccountManager accountManager, ExternalIds externalIds, Provider<CurrentUser> self) {
     this.accountManager = accountManager;
     this.externalIds = externalIds;
     this.self = self;
-    this.dbProvider = dbProvider;
   }
 
   @Override
@@ -68,7 +62,7 @@
 
     Map<ExternalId.Key, ExternalId> externalIdMap =
         externalIds
-            .byAccount(dbProvider.get(), resource.getUser().getAccountId())
+            .byAccount(resource.getUser().getAccountId())
             .stream()
             .collect(toMap(i -> i.key(), i -> i));
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetExternalIds.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetExternalIds.java
index 12de82c..46c1dd8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetExternalIds.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetExternalIds.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
@@ -38,18 +37,12 @@
 
 @Singleton
 public class GetExternalIds implements RestReadView<AccountResource> {
-  private final Provider<ReviewDb> db;
   private final ExternalIds externalIds;
   private final Provider<CurrentUser> self;
   private final AuthConfig authConfig;
 
   @Inject
-  GetExternalIds(
-      Provider<ReviewDb> db,
-      ExternalIds externalIds,
-      Provider<CurrentUser> self,
-      AuthConfig authConfig) {
-    this.db = db;
+  GetExternalIds(ExternalIds externalIds, Provider<CurrentUser> self, AuthConfig authConfig) {
     this.externalIds = externalIds;
     this.self = self;
     this.authConfig = authConfig;
@@ -62,7 +55,7 @@
       throw new AuthException("not allowed to get external IDs");
     }
 
-    Collection<ExternalId> ids = externalIds.byAccount(db.get(), resource.getUser().getAccountId());
+    Collection<ExternalId> ids = externalIds.byAccount(resource.getUser().getAccountId());
     if (ids.isEmpty()) {
       return ImmutableList.of();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
index c06e5a3..395f078 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutHttpPassword.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestModifyView;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.PutHttpPassword.Input;
@@ -59,7 +58,6 @@
   }
 
   private final Provider<CurrentUser> self;
-  private final Provider<ReviewDb> dbProvider;
   private final PermissionBackend permissionBackend;
   private final AccountCache accountCache;
   private final ExternalIds externalIds;
@@ -68,13 +66,11 @@
   @Inject
   PutHttpPassword(
       Provider<CurrentUser> self,
-      Provider<ReviewDb> dbProvider,
       PermissionBackend permissionBackend,
       AccountCache accountCache,
       ExternalIds externalIds,
       ExternalIdsUpdate.User externalIdsUpdate) {
     this.self = self;
-    this.dbProvider = dbProvider;
     this.permissionBackend = permissionBackend;
     this.accountCache = accountCache;
     this.externalIds = externalIds;
@@ -114,15 +110,13 @@
       throw new ResourceConflictException("username must be set");
     }
 
-    ExternalId extId =
-        externalIds.get(
-            dbProvider.get(), ExternalId.Key.create(SCHEME_USERNAME, user.getUserName()));
+    ExternalId extId = externalIds.get(ExternalId.Key.create(SCHEME_USERNAME, user.getUserName()));
     if (extId == null) {
       throw new ResourceNotFoundException();
     }
     ExternalId newExtId =
         ExternalId.createWithPassword(extId.key(), extId.accountId(), extId.email(), newPassword);
-    externalIdsUpdate.create().upsert(dbProvider.get(), newExtId);
+    externalIdsUpdate.create().upsert(newExtId);
     accountCache.evict(user.getAccountId());
 
     return Strings.isNullOrEmpty(newPassword) ? Response.<String>none() : Response.ok(newPassword);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java
index 57bff65..a73bdd9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PutUsername.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.PutUsername.Input;
 import com.google.gerrit.server.permissions.GlobalPermission;
@@ -45,20 +44,17 @@
   private final ChangeUserName.Factory changeUserNameFactory;
   private final PermissionBackend permissionBackend;
   private final Realm realm;
-  private final Provider<ReviewDb> db;
 
   @Inject
   PutUsername(
       Provider<CurrentUser> self,
       ChangeUserName.Factory changeUserNameFactory,
       PermissionBackend permissionBackend,
-      Realm realm,
-      Provider<ReviewDb> db) {
+      Realm realm) {
     this.self = self;
     this.changeUserNameFactory = changeUserNameFactory;
     this.permissionBackend = permissionBackend;
     this.realm = realm;
-    this.db = db;
   }
 
   @Override
@@ -79,7 +75,7 @@
     }
 
     try {
-      changeUserNameFactory.create(db.get(), rsrc.getUser(), input.username).call();
+      changeUserNameFactory.create(rsrc.getUser(), input.username).call();
     } catch (IllegalStateException e) {
       if (ChangeUserName.USERNAME_CANNOT_BE_CHANGED.equals(e.getMessage())) {
         throw new MethodNotAllowedException(e.getMessage());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
index b4fef67..4aabf59 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -16,20 +16,16 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.stream.Collectors.toSet;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.server.account.HashedPassword;
 import java.io.Serializable;
-import java.util.Collection;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
@@ -75,10 +71,6 @@
       return new AutoValue_ExternalId_Key(Strings.emptyToNull(scheme), id);
     }
 
-    public static ExternalId.Key from(AccountExternalId.Key externalIdKey) {
-      return parse(externalIdKey.get());
-    }
-
     /**
      * Parses an external ID key from a string in the format "scheme:id" or "id".
      *
@@ -92,11 +84,6 @@
       return create(externalId.substring(0, c), externalId.substring(c + 1));
     }
 
-    public static Set<AccountExternalId.Key> toAccountExternalIdKeys(
-        Collection<ExternalId.Key> extIdKeys) {
-      return extIdKeys.stream().map(k -> k.asAccountExternalIdKey()).collect(toSet());
-    }
-
     public abstract @Nullable String scheme();
 
     public abstract String id();
@@ -105,13 +92,6 @@
       return scheme.equals(scheme());
     }
 
-    public AccountExternalId.Key asAccountExternalIdKey() {
-      if (scheme() != null) {
-        return new AccountExternalId.Key(scheme(), id());
-      }
-      return new AccountExternalId.Key(id());
-    }
-
     /**
      * Returns the SHA1 of the external ID that is used as note ID in the refs/meta/external-ids
      * notes branch.
@@ -281,29 +261,6 @@
         String.format("Invalid external ID config for note '%s': %s", noteId, message));
   }
 
-  public static ExternalId from(AccountExternalId externalId) {
-    if (externalId == null) {
-      return null;
-    }
-
-    return new AutoValue_ExternalId(
-        ExternalId.Key.parse(externalId.getExternalId()),
-        externalId.getAccountId(),
-        Strings.emptyToNull(externalId.getEmailAddress()),
-        Strings.emptyToNull(externalId.getPassword()));
-  }
-
-  public static Set<ExternalId> from(Collection<AccountExternalId> externalIds) {
-    if (externalIds == null) {
-      return ImmutableSet.of();
-    }
-    return externalIds.stream().map(ExternalId::from).collect(toSet());
-  }
-
-  public static Set<AccountExternalId> toAccountExternalIds(Collection<ExternalId> extIds) {
-    return extIds.stream().map(e -> e.asAccountExternalId()).collect(toSet());
-  }
-
   public abstract Key key();
 
   public abstract Account.Id accountId();
@@ -316,13 +273,6 @@
     return key().isScheme(scheme);
   }
 
-  public AccountExternalId asAccountExternalId() {
-    AccountExternalId extId = new AccountExternalId(accountId(), key().asAccountExternalIdKey());
-    extId.setEmailAddress(email());
-    extId.setPassword(password());
-    return extId;
-  }
-
   /**
    * Exports this external ID as Git config file text.
    *
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
index e25c36f..ead2c1c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdReader.java
@@ -24,18 +24,14 @@
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.metrics.Timer0;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.AllUsersName;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.util.HashSet;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -46,7 +42,7 @@
 import org.slf4j.LoggerFactory;
 
 /**
- * Class to read external IDs from ReviewDb or NoteDb.
+ * Class to read external IDs from NoteDb.
  *
  * <p>In NoteDb external IDs are stored in the All-Users repository in a Git Notes branch called
  * refs/meta/external-ids where the sha1 of the external ID is used as note name. Each note content
@@ -78,7 +74,6 @@
     return NoteMap.newEmptyMap();
   }
 
-  private final boolean readFromGit;
   private final GitRepositoryManager repoManager;
   private final AllUsersName allUsersName;
   private boolean failOnLoad = false;
@@ -86,11 +81,7 @@
 
   @Inject
   ExternalIdReader(
-      @GerritServerConfig Config cfg,
-      GitRepositoryManager repoManager,
-      AllUsersName allUsersName,
-      MetricMaker metricMaker) {
-    this.readFromGit = cfg.getBoolean("user", null, "readExternalIdsFromGit", false);
+      GitRepositoryManager repoManager, AllUsersName allUsersName, MetricMaker metricMaker) {
     this.repoManager = repoManager;
     this.allUsersName = allUsersName;
     this.readAllLatency =
@@ -106,10 +97,6 @@
     this.failOnLoad = failOnLoad;
   }
 
-  boolean readFromGit() {
-    return readFromGit;
-  }
-
   ObjectId readRevision() throws IOException {
     try (Repository repo = repoManager.openRepository(allUsersName)) {
       return readRevision(repo);
@@ -117,16 +104,12 @@
   }
 
   /** Reads and returns all external IDs. */
-  Set<ExternalId> all(ReviewDb db) throws IOException, OrmException {
+  Set<ExternalId> all() throws IOException {
     checkReadEnabled();
 
-    if (readFromGit) {
-      try (Repository repo = repoManager.openRepository(allUsersName)) {
-        return all(repo, readRevision(repo));
-      }
+    try (Repository repo = repoManager.openRepository(allUsersName)) {
+      return all(repo, readRevision(repo));
     }
-
-    return ExternalId.from(db.accountExternalIds().all().toList());
   }
 
   /**
@@ -166,22 +149,18 @@
 
   /** Reads and returns the specified external ID. */
   @Nullable
-  ExternalId get(ReviewDb db, ExternalId.Key key)
-      throws IOException, ConfigInvalidException, OrmException {
+  ExternalId get(ExternalId.Key key) throws IOException, ConfigInvalidException {
     checkReadEnabled();
 
-    if (readFromGit) {
-      try (Repository repo = repoManager.openRepository(allUsersName);
-          RevWalk rw = new RevWalk(repo)) {
-        ObjectId rev = readRevision(repo);
-        if (rev.equals(ObjectId.zeroId())) {
-          return null;
-        }
-
-        return parse(key, rw, rev);
+    try (Repository repo = repoManager.openRepository(allUsersName);
+        RevWalk rw = new RevWalk(repo)) {
+      ObjectId rev = readRevision(repo);
+      if (rev.equals(ObjectId.zeroId())) {
+        return null;
       }
+
+      return parse(key, rw, rev);
     }
-    return ExternalId.from(db.accountExternalIds().get(key.asAccountExternalIdKey()));
   }
 
   /** Reads and returns the specified external ID from the given revision. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIds.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIds.java
index b77fed8..5003a35 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIds.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIds.java
@@ -18,8 +18,6 @@
 
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
@@ -44,8 +42,8 @@
   }
 
   /** Returns all external IDs. */
-  public Set<ExternalId> all(ReviewDb db) throws IOException, OrmException {
-    return externalIdReader.all(db);
+  public Set<ExternalId> all() throws IOException {
+    return externalIdReader.all();
   }
 
   /** Returns all external IDs from the specified revision of the refs/meta/external-ids branch. */
@@ -55,9 +53,8 @@
 
   /** Returns the specified external ID. */
   @Nullable
-  public ExternalId get(ReviewDb db, ExternalId.Key key)
-      throws IOException, ConfigInvalidException, OrmException {
-    return externalIdReader.get(db, key);
+  public ExternalId get(ExternalId.Key key) throws IOException, ConfigInvalidException {
+    return externalIdReader.get(key);
   }
 
   /** Returns the specified external ID from the given revision. */
@@ -68,26 +65,16 @@
   }
 
   /** Returns the external IDs of the specified account. */
-  public Set<ExternalId> byAccount(ReviewDb db, Account.Id accountId)
-      throws IOException, OrmException {
-    if (externalIdReader.readFromGit()) {
-      return externalIdCache.byAccount(accountId);
-    }
-
-    return ExternalId.from(db.accountExternalIds().byAccount(accountId).toList());
+  public Set<ExternalId> byAccount(Account.Id accountId) throws IOException {
+    return externalIdCache.byAccount(accountId);
   }
 
   /** Returns the external IDs of the specified account that have the given scheme. */
-  public Set<ExternalId> byAccount(ReviewDb db, Account.Id accountId, String scheme)
-      throws IOException, OrmException {
-    return byAccount(db, accountId).stream().filter(e -> e.key().isScheme(scheme)).collect(toSet());
+  public Set<ExternalId> byAccount(Account.Id accountId, String scheme) throws IOException {
+    return byAccount(accountId).stream().filter(e -> e.key().isScheme(scheme)).collect(toSet());
   }
 
-  public Set<ExternalId> byEmail(ReviewDb db, String email) throws IOException, OrmException {
-    if (externalIdReader.readFromGit()) {
-      return externalIdCache.byEmail(email);
-    }
-
-    return ExternalId.from(db.accountExternalIds().byEmailAddress(email).toList());
+  public Set<ExternalId> byEmail(String email) throws IOException {
+    return externalIdCache.byEmail(email);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsBatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsBatchUpdate.java
index 492866d..e35b0c3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsBatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/externalids/ExternalIdsBatchUpdate.java
@@ -14,10 +14,7 @@
 
 package com.google.gerrit.server.account.externalids;
 
-import static com.google.gerrit.server.account.externalids.ExternalId.toAccountExternalIds;
-
 import com.google.common.collect.ImmutableSet;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -39,8 +36,8 @@
  *
  * <p>For NoteDb all updates will result in a single commit to the refs/meta/external-ids branch.
  * This means callers can prepare many updates by invoking {@link #replace(ExternalId, ExternalId)}
- * multiple times and when {@link ExternalIdsBatchUpdate#commit(ReviewDb, String)} is invoked a
- * single NoteDb commit is created that contains all the prepared updates.
+ * multiple times and when {@link ExternalIdsBatchUpdate#commit(String)} is invoked a single NoteDb
+ * commit is created that contains all the prepared updates.
  */
 public class ExternalIdsBatchUpdate {
   private final GitRepositoryManager repoManager;
@@ -65,7 +62,7 @@
   /**
    * Adds an external ID replacement to the batch.
    *
-   * <p>The actual replacement is only done when {@link #commit(ReviewDb, String)} is invoked.
+   * <p>The actual replacement is only done when {@link #commit(String)} is invoked.
    */
   public void replace(ExternalId extIdToDelete, ExternalId extIdToAdd) {
     ExternalIdsUpdate.checkSameAccount(ImmutableSet.of(extIdToDelete, extIdToAdd));
@@ -85,15 +82,12 @@
    *
    * <p>For NoteDb a single commit is created that contains all the external ID updates.
    */
-  public void commit(ReviewDb db, String commitMessage)
+  public void commit(String commitMessage)
       throws IOException, OrmException, ConfigInvalidException {
     if (toDelete.isEmpty() && toAdd.isEmpty()) {
       return;
     }
 
-    db.accountExternalIds().delete(toAccountExternalIds(toDelete));
-    db.accountExternalIds().insert(toAccountExternalIds(toAdd));
-
     try (Repository repo = repoManager.openRepository(allUsersName);
         RevWalk rw = new RevWalk(repo);
         ObjectInserter ins = repo.newObjectInserter()) {
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 003928f..a1d6eeb 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
@@ -16,8 +16,6 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.common.base.Preconditions.checkState;
-import static com.google.gerrit.server.account.externalids.ExternalId.Key.toAccountExternalIdKeys;
-import static com.google.gerrit.server.account.externalids.ExternalId.toAccountExternalIds;
 import static com.google.gerrit.server.account.externalids.ExternalIdReader.MAX_NOTE_SZ;
 import static com.google.gerrit.server.account.externalids.ExternalIdReader.readNoteMap;
 import static com.google.gerrit.server.account.externalids.ExternalIdReader.readRevision;
@@ -42,7 +40,6 @@
 import com.google.gerrit.metrics.MetricMaker;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AllUsersName;
@@ -252,9 +249,8 @@
    *
    * <p>If the external ID already exists, the insert fails with {@link OrmDuplicateKeyException}.
    */
-  public void insert(ReviewDb db, ExternalId extId)
-      throws IOException, ConfigInvalidException, OrmException {
-    insert(db, Collections.singleton(extId));
+  public void insert(ExternalId extId) throws IOException, ConfigInvalidException, OrmException {
+    insert(Collections.singleton(extId));
   }
 
   /**
@@ -263,10 +259,8 @@
    * <p>If any of the external ID already exists, the insert fails with {@link
    * OrmDuplicateKeyException}.
    */
-  public void insert(ReviewDb db, Collection<ExternalId> extIds)
+  public void insert(Collection<ExternalId> extIds)
       throws IOException, ConfigInvalidException, OrmException {
-    db.accountExternalIds().insert(toAccountExternalIds(extIds));
-
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
@@ -282,9 +276,8 @@
    *
    * <p>If the external ID already exists, it is overwritten, otherwise it is inserted.
    */
-  public void upsert(ReviewDb db, ExternalId extId)
-      throws IOException, ConfigInvalidException, OrmException {
-    upsert(db, Collections.singleton(extId));
+  public void upsert(ExternalId extId) throws IOException, ConfigInvalidException, OrmException {
+    upsert(Collections.singleton(extId));
   }
 
   /**
@@ -292,10 +285,8 @@
    *
    * <p>If any of the external IDs already exists, it is overwritten. New external IDs are inserted.
    */
-  public void upsert(ReviewDb db, Collection<ExternalId> extIds)
+  public void upsert(Collection<ExternalId> extIds)
       throws IOException, ConfigInvalidException, OrmException {
-    db.accountExternalIds().upsert(toAccountExternalIds(extIds));
-
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
@@ -312,9 +303,8 @@
    * @throws IllegalStateException is thrown if there is an existing external ID that has the same
    *     key, but otherwise doesn't match the specified external ID.
    */
-  public void delete(ReviewDb db, ExternalId extId)
-      throws IOException, ConfigInvalidException, OrmException {
-    delete(db, Collections.singleton(extId));
+  public void delete(ExternalId extId) throws IOException, ConfigInvalidException, OrmException {
+    delete(Collections.singleton(extId));
   }
 
   /**
@@ -324,10 +314,8 @@
    *     key as any of the external IDs that should be deleted, but otherwise doesn't match the that
    *     external ID.
    */
-  public void delete(ReviewDb db, Collection<ExternalId> extIds)
+  public void delete(Collection<ExternalId> extIds)
       throws IOException, ConfigInvalidException, OrmException {
-    db.accountExternalIds().delete(toAccountExternalIds(extIds));
-
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
@@ -344,9 +332,9 @@
    * @throws IllegalStateException is thrown if the external ID does not belong to the specified
    *     account.
    */
-  public void delete(ReviewDb db, Account.Id accountId, ExternalId.Key extIdKey)
+  public void delete(Account.Id accountId, ExternalId.Key extIdKey)
       throws IOException, ConfigInvalidException, OrmException {
-    delete(db, accountId, Collections.singleton(extIdKey));
+    delete(accountId, Collections.singleton(extIdKey));
   }
 
   /**
@@ -355,10 +343,8 @@
    * @throws IllegalStateException is thrown if any of the external IDs does not belong to the
    *     specified account.
    */
-  public void delete(ReviewDb db, Account.Id accountId, Collection<ExternalId.Key> extIdKeys)
+  public void delete(Account.Id accountId, Collection<ExternalId.Key> extIdKeys)
       throws IOException, ConfigInvalidException, OrmException {
-    db.accountExternalIds().deleteKeys(toAccountExternalIdKeys(extIdKeys));
-
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
@@ -374,10 +360,8 @@
    *
    * <p>The external IDs are deleted regardless of which account they belong to.
    */
-  public void deleteByKeys(ReviewDb db, Collection<ExternalId.Key> extIdKeys)
+  public void deleteByKeys(Collection<ExternalId.Key> extIdKeys)
       throws IOException, ConfigInvalidException, OrmException {
-    db.accountExternalIds().deleteKeys(toAccountExternalIdKeys(extIdKeys));
-
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
@@ -389,9 +373,9 @@
   }
 
   /** Deletes all external IDs of the specified account. */
-  public void deleteAll(ReviewDb db, Account.Id accountId)
+  public void deleteAll(Account.Id accountId)
       throws IOException, ConfigInvalidException, OrmException {
-    delete(db, externalIds.byAccount(db, accountId));
+    delete(externalIds.byAccount(accountId));
   }
 
   /**
@@ -406,16 +390,10 @@
    *     the specified account.
    */
   public void replace(
-      ReviewDb db,
-      Account.Id accountId,
-      Collection<ExternalId.Key> toDelete,
-      Collection<ExternalId> toAdd)
+      Account.Id accountId, Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
       throws IOException, ConfigInvalidException, OrmException {
     checkSameAccount(toAdd, accountId);
 
-    db.accountExternalIds().deleteKeys(toAccountExternalIdKeys(toDelete));
-    db.accountExternalIds().insert(toAccountExternalIds(toAdd));
-
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
@@ -440,12 +418,8 @@
    *
    * <p>The external IDs are replaced regardless of which account they belong to.
    */
-  public void replaceByKeys(
-      ReviewDb db, Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
+  public void replaceByKeys(Collection<ExternalId.Key> toDelete, Collection<ExternalId> toAdd)
       throws IOException, ConfigInvalidException, OrmException {
-    db.accountExternalIds().deleteKeys(toAccountExternalIdKeys(toDelete));
-    db.accountExternalIds().insert(toAccountExternalIds(toAdd));
-
     RefsMetaExternalIdsUpdate u =
         updateNoteMap(
             o -> {
@@ -466,9 +440,9 @@
    * @throws IllegalStateException is thrown if the specified external IDs belong to different
    *     accounts.
    */
-  public void replace(ReviewDb db, ExternalId toDelete, ExternalId toAdd)
+  public void replace(ExternalId toDelete, ExternalId toAdd)
       throws IOException, ConfigInvalidException, OrmException {
-    replace(db, Collections.singleton(toDelete), Collections.singleton(toAdd));
+    replace(Collections.singleton(toDelete), Collections.singleton(toAdd));
   }
 
   /**
@@ -482,7 +456,7 @@
    * @throws IllegalStateException is thrown if the specified external IDs belong to different
    *     accounts.
    */
-  public void replace(ReviewDb db, Collection<ExternalId> toDelete, Collection<ExternalId> toAdd)
+  public void replace(Collection<ExternalId> toDelete, Collection<ExternalId> toAdd)
       throws IOException, ConfigInvalidException, OrmException {
     Account.Id accountId = checkSameAccount(Iterables.concat(toDelete, toAdd));
     if (accountId == null) {
@@ -490,7 +464,7 @@
       return;
     }
 
-    replace(db, accountId, toDelete.stream().map(e -> e.key()).collect(toSet()), toAdd);
+    replace(accountId, toDelete.stream().map(e -> e.key()).collect(toSet()), toAdd);
   }
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
index 5338e89..f4ea3b0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/ChangeApiImpl.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.api.changes.AbandonInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
 import com.google.gerrit.extensions.api.changes.AddReviewerResult;
@@ -66,16 +67,17 @@
 import com.google.gerrit.server.change.Move;
 import com.google.gerrit.server.change.Mute;
 import com.google.gerrit.server.change.PostHashtags;
+import com.google.gerrit.server.change.PostPrivate;
 import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.change.PublishDraftPatchSet;
 import com.google.gerrit.server.change.PutAssignee;
-import com.google.gerrit.server.change.PutPrivate;
 import com.google.gerrit.server.change.PutTopic;
 import com.google.gerrit.server.change.Rebase;
 import com.google.gerrit.server.change.Restore;
 import com.google.gerrit.server.change.Revert;
 import com.google.gerrit.server.change.Reviewers;
 import com.google.gerrit.server.change.Revisions;
+import com.google.gerrit.server.change.SetPrivateOp;
 import com.google.gerrit.server.change.SetReadyForReview;
 import com.google.gerrit.server.change.SetWorkInProgress;
 import com.google.gerrit.server.change.SubmittedTogether;
@@ -129,7 +131,7 @@
   private final Check check;
   private final Index index;
   private final Move move;
-  private final PutPrivate putPrivate;
+  private final PostPrivate postPrivate;
   private final DeletePrivate deletePrivate;
   private final Ignore ignore;
   private final Unignore unignore;
@@ -172,7 +174,7 @@
       Check check,
       Index index,
       Move move,
-      PutPrivate putPrivate,
+      PostPrivate postPrivate,
       DeletePrivate deletePrivate,
       Ignore ignore,
       Unignore unignore,
@@ -213,7 +215,7 @@
     this.check = check;
     this.index = index;
     this.move = move;
-    this.putPrivate = putPrivate;
+    this.postPrivate = postPrivate;
     this.deletePrivate = deletePrivate;
     this.ignore = ignore;
     this.unignore = unignore;
@@ -302,12 +304,13 @@
   }
 
   @Override
-  public void setPrivate(boolean value) throws RestApiException {
+  public void setPrivate(boolean value, @Nullable String message) throws RestApiException {
     try {
+      SetPrivateOp.Input input = new SetPrivateOp.Input(message);
       if (value) {
-        putPrivate.apply(change, null);
+        postPrivate.apply(change, input);
       } else {
-        deletePrivate.apply(change, null);
+        deletePrivate.apply(change, input);
       }
     } catch (Exception e) {
       throw asRestApiException("Cannot change private status", e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/CommentApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/CommentApiImpl.java
index 243833a..6a2501e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/CommentApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/CommentApiImpl.java
@@ -17,9 +17,11 @@
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
 import com.google.gerrit.extensions.api.changes.CommentApi;
+import com.google.gerrit.extensions.api.changes.DeleteCommentInput;
 import com.google.gerrit.extensions.common.CommentInfo;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.change.CommentResource;
+import com.google.gerrit.server.change.DeleteComment;
 import com.google.gerrit.server.change.GetComment;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -30,11 +32,14 @@
   }
 
   private final GetComment getComment;
+  private final DeleteComment deleteComment;
   private final CommentResource comment;
 
   @Inject
-  CommentApiImpl(GetComment getComment, @Assisted CommentResource comment) {
+  CommentApiImpl(
+      GetComment getComment, DeleteComment deleteComment, @Assisted CommentResource comment) {
     this.getComment = getComment;
+    this.deleteComment = deleteComment;
     this.comment = comment;
   }
 
@@ -46,4 +51,13 @@
       throw asRestApiException("Cannot retrieve comment", e);
     }
   }
+
+  @Override
+  public CommentInfo delete(DeleteCommentInput input) throws RestApiException {
+    try {
+      return deleteComment.apply(comment, input);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot delete comment", e);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
index 2daf1dc..eada51b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/api/changes/DraftApiImpl.java
@@ -16,9 +16,11 @@
 
 import static com.google.gerrit.server.api.ApiUtil.asRestApiException;
 
+import com.google.gerrit.extensions.api.changes.DeleteCommentInput;
 import com.google.gerrit.extensions.api.changes.DraftApi;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.NotImplementedException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.server.change.DeleteDraftComment;
 import com.google.gerrit.server.change.DraftCommentResource;
@@ -75,4 +77,9 @@
       throw asRestApiException("Cannot delete draft", e);
     }
   }
+
+  @Override
+  public CommentInfo delete(DeleteCommentInput input) {
+    throw new NotImplementedException();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index 9bcf3d6..4685dc0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.extensions.client.AuthType;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AbstractRealm;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AuthRequest;
@@ -36,7 +35,6 @@
 import com.google.gerrit.server.auth.AuthenticationUnavailableException;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
@@ -319,22 +317,17 @@
   }
 
   static class UserLoader extends CacheLoader<String, Optional<Account.Id>> {
-    private final SchemaFactory<ReviewDb> schema;
     private final ExternalIds externalIds;
 
     @Inject
-    UserLoader(SchemaFactory<ReviewDb> schema, ExternalIds externalIds) {
-      this.schema = schema;
+    UserLoader(ExternalIds externalIds) {
       this.externalIds = externalIds;
     }
 
     @Override
     public Optional<Account.Id> load(String username) throws Exception {
-      try (ReviewDb db = schema.open()) {
-        return Optional.ofNullable(
-                externalIds.get(db, ExternalId.Key.create(SCHEME_GERRIT, username)))
-            .map(ExternalId::accountId);
-      }
+      return Optional.ofNullable(externalIds.get(ExternalId.Key.create(SCHEME_GERRIT, username)))
+          .map(ExternalId::accountId);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java
index 40c8515..f7fc576 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CommentResource.java
@@ -48,4 +48,8 @@
   Account.Id getAuthorId() {
     return comment.author.getId();
   }
+
+  RevisionResource getRevisionResource() {
+    return rev;
+  }
 }
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 3c404d6..599ce5e 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
@@ -180,7 +180,7 @@
     }
 
     RefControl refControl = rsrc.getControl().controlForRef(refName);
-    if (!refControl.canUpload() || !refControl.canRead()) {
+    if (!refControl.canUpload() || !refControl.isVisible()) {
       throw new AuthException("cannot upload review");
     }
 
@@ -194,11 +194,11 @@
       if (input.baseChange != null) {
         List<ChangeControl> ctls = changeFinder.find(input.baseChange, rsrc.getControl().getUser());
         if (ctls.size() != 1) {
-          throw new InvalidChangeOperationException("Base change not found: " + input.baseChange);
+          throw new UnprocessableEntityException("Base change not found: " + input.baseChange);
         }
         ChangeControl ctl = Iterables.getOnlyElement(ctls);
         if (!ctl.isVisible(db.get())) {
-          throw new InvalidChangeOperationException("Base change not found: " + input.baseChange);
+          throw new UnprocessableEntityException("Base change not found: " + input.baseChange);
         }
         PatchSet ps = psUtil.current(db.get(), ctl.getNotes());
         parentCommit = ObjectId.fromString(ps.getRevision().get());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteComment.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteComment.java
new file mode 100644
index 0000000..b0b222b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteComment.java
@@ -0,0 +1,145 @@
+// 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.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.api.changes.DeleteCommentInput;
+import com.google.gerrit.extensions.common.CommentInfo;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.PatchSetUtil;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.update.BatchUpdate;
+import com.google.gerrit.server.update.BatchUpdateOp;
+import com.google.gerrit.server.update.ChangeContext;
+import com.google.gerrit.server.update.UpdateException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+@Singleton
+public class DeleteComment implements RestModifyView<CommentResource, DeleteCommentInput> {
+
+  private final Provider<CurrentUser> userProvider;
+  private final Provider<ReviewDb> dbProvider;
+  private final PermissionBackend permissionBackend;
+  private final BatchUpdate.Factory batchUpdateFactory;
+  private final CommentsUtil commentsUtil;
+  private final PatchSetUtil psUtil;
+  private final BatchUpdate.Factory updateFactory;
+  private final PatchListCache patchListCache;
+  private final Provider<CommentJson> commentJson;
+  private final ChangeNotes.Factory notesFactory;
+
+  @Inject
+  public DeleteComment(
+      Provider<CurrentUser> userProvider,
+      Provider<ReviewDb> dbProvider,
+      PermissionBackend permissionBackend,
+      BatchUpdate.Factory batchUpdateFactory,
+      CommentsUtil commentsUtil,
+      PatchSetUtil psUtil,
+      BatchUpdate.Factory updateFactory,
+      PatchListCache patchListCache,
+      Provider<CommentJson> commentJson,
+      ChangeNotes.Factory notesFactory) {
+    this.userProvider = userProvider;
+    this.dbProvider = dbProvider;
+    this.permissionBackend = permissionBackend;
+    this.batchUpdateFactory = batchUpdateFactory;
+    this.commentsUtil = commentsUtil;
+    this.psUtil = psUtil;
+    this.updateFactory = updateFactory;
+    this.patchListCache = patchListCache;
+    this.commentJson = commentJson;
+    this.notesFactory = notesFactory;
+  }
+
+  @Override
+  public CommentInfo apply(CommentResource rsrc, DeleteCommentInput input)
+      throws RestApiException, IOException, ConfigInvalidException, OrmException,
+          PermissionBackendException, UpdateException {
+    CurrentUser user = userProvider.get();
+    permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
+
+    String newMessage = getCommentNewMessage(user.asIdentifiedUser().getName(), input.reason);
+    DeleteCommentOp deleteCommentOp = new DeleteCommentOp(rsrc, newMessage);
+    try (BatchUpdate batchUpdate =
+        batchUpdateFactory.create(
+            dbProvider.get(), rsrc.getRevisionResource().getProject(), user, TimeUtil.nowTs())) {
+      batchUpdate.addOp(rsrc.getRevisionResource().getChange().getId(), deleteCommentOp).execute();
+    }
+
+    ChangeNotes updatedNotes =
+        notesFactory.createChecked(rsrc.getRevisionResource().getChange().getId());
+    List<Comment> changeComments = commentsUtil.publishedByChange(dbProvider.get(), updatedNotes);
+    Optional<Comment> updatedComment =
+        changeComments.stream().filter(c -> c.key.equals(rsrc.getComment().key)).findFirst();
+    if (!updatedComment.isPresent()) {
+      // This should not happen as this endpoint should not remove the whole comment.
+      throw new ResourceNotFoundException("comment not found: " + rsrc.getComment().key);
+    }
+
+    return commentJson.get().newCommentFormatter().format(updatedComment.get());
+  }
+
+  private static String getCommentNewMessage(String name, String reason) {
+    StringBuilder stringBuilder = new StringBuilder("Comment removed by: ").append(name);
+    if (!Strings.isNullOrEmpty(reason)) {
+      stringBuilder.append("; Reason: ").append(reason);
+    }
+    return stringBuilder.toString();
+  }
+
+  private class DeleteCommentOp implements BatchUpdateOp {
+    private final CommentResource rsrc;
+    private final String newMessage;
+
+    DeleteCommentOp(CommentResource rsrc, String newMessage) {
+      this.rsrc = rsrc;
+      this.newMessage = newMessage;
+    }
+
+    @Override
+    public boolean updateChange(ChangeContext ctx)
+        throws ResourceConflictException, OrmException, ResourceNotFoundException {
+      PatchSet.Id psId = ctx.getChange().currentPatchSetId();
+      commentsUtil.deleteCommentByRewritingHistory(
+          ctx.getDb(),
+          ctx.getUpdate(psId),
+          rsrc.getComment().key,
+          rsrc.getPatchSet().getId(),
+          newMessage);
+      return true;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java
index a951d66..7819a29 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeletePrivate.java
@@ -33,10 +33,7 @@
 
 @Singleton
 public class DeletePrivate
-    extends RetryingRestModifyView<ChangeResource, DeletePrivate.Input, Response<String>>
-    implements UiAction<ChangeResource> {
-  public static class Input {}
-
+    extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, Response<String>> {
   private final ChangeMessagesUtil cmUtil;
   private final Provider<ReviewDb> dbProvider;
 
@@ -49,7 +46,7 @@
 
   @Override
   protected Response<String> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, DeletePrivate.Input input)
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, SetPrivateOp.Input input)
       throws RestApiException, UpdateException {
     if (!rsrc.isUserOwner()) {
       throw new AuthException("not allowed to unmark private");
@@ -60,7 +57,7 @@
     }
 
     ChangeControl control = rsrc.getControl();
-    SetPrivateOp op = new SetPrivateOp(cmUtil, false);
+    SetPrivateOp op = new SetPrivateOp(cmUtil, false, input);
     try (BatchUpdate u =
         updateFactory.create(
             dbProvider.get(),
@@ -73,11 +70,20 @@
     return Response.none();
   }
 
-  @Override
-  public Description getDescription(ChangeResource rsrc) {
-    return new UiAction.Description()
-        .setLabel("Unmark private")
-        .setTitle("Unmark change as private")
-        .setVisible(rsrc.getChange().isPrivate() && rsrc.isUserOwner());
+  public static class DeletePrivateByPost extends DeletePrivate
+      implements UiAction<ChangeResource> {
+    @Inject
+    DeletePrivateByPost(
+        Provider<ReviewDb> dbProvider, RetryHelper retryHelper, ChangeMessagesUtil cmUtil) {
+      super(dbProvider, retryHelper, cmUtil);
+    }
+
+    @Override
+    public Description getDescription(ChangeResource rsrc) {
+      return new UiAction.Description()
+          .setLabel("Unmark private")
+          .setTitle("Unmark change as private")
+          .setVisible(rsrc.getChange().isPrivate() && rsrc.isUserOwner());
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
index e812002..01401b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileContentUtil.java
@@ -75,7 +75,7 @@
    * @param revstr An {@code ObjectId} specifying the commit.
    * @param path A string specifying the filepath.
    * @param parent A 1-based parent index to get the content from instead. Null if the content
-   *     should be obtained from {@param revstr} instead.
+   *     should be obtained from {@code revstr} instead.
    * @return Content of the file as {@code BinaryResult}.
    * @throws ResourceNotFoundException
    * @throws IOException
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
index ebc9971..1a0d118 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Files.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.extensions.restapi.RestView;
 import com.google.gerrit.reviewdb.client.Account;
+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;
@@ -252,11 +253,18 @@
           ObjectReader reader = git.newObjectReader();
           RevWalk rw = new RevWalk(reader);
           TreeWalk tw = new TreeWalk(reader)) {
-        PatchList oldList =
-            patchListCache.get(
-                resource.getChange(), psUtil.get(db.get(), resource.getNotes(), old));
+        Change change = resource.getChange();
+        PatchSet patchSet = psUtil.get(db.get(), resource.getNotes(), old);
+        if (patchSet == null) {
+          throw new PatchListNotAvailableException(
+              String.format(
+                  "patch set %s of change %s not found",
+                  old.get(), change.getId().get()));
+        }
 
-        PatchList curList = patchListCache.get(resource.getChange(), resource.getPatchSet());
+        PatchList oldList = patchListCache.get(change, patchSet);
+
+        PatchList curList = patchListCache.get(change, resource.getPatchSet());
 
         int sz = paths.size();
         List<String> pathList = Lists.newArrayListWithCapacity(sz);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
index 5ddf9e9..43a487b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.gerrit.server.account.AccountLoader;
+import com.google.gerrit.server.change.DeletePrivate.DeletePrivateByPost;
 import com.google.gerrit.server.change.Reviewed.DeleteReviewed;
 import com.google.gerrit.server.change.Reviewed.PutReviewed;
 
@@ -85,7 +86,8 @@
     post(CHANGE_KIND, "index").to(Index.class);
     post(CHANGE_KIND, "rebuild.notedb").to(Rebuild.class);
     post(CHANGE_KIND, "move").to(Move.class);
-    put(CHANGE_KIND, "private").to(PutPrivate.class);
+    post(CHANGE_KIND, "private").to(PostPrivate.class);
+    post(CHANGE_KIND, "private.delete").to(DeletePrivateByPost.class);
     delete(CHANGE_KIND, "private").to(DeletePrivate.class);
     put(CHANGE_KIND, "ignore").to(Ignore.class);
     put(CHANGE_KIND, "unignore").to(Unignore.class);
@@ -136,6 +138,8 @@
 
     child(REVISION_KIND, "comments").to(Comments.class);
     get(COMMENT_KIND).to(GetComment.class);
+    delete(COMMENT_KIND).to(DeleteComment.class);
+    post(COMMENT_KIND, "delete").to(DeleteComment.class);
 
     child(REVISION_KIND, "robotcomments").to(RobotComments.class);
     get(ROBOT_COMMENT_KIND).to(GetRobotComment.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutPrivate.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostPrivate.java
similarity index 72%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/PutPrivate.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/change/PostPrivate.java
index bd2bf05..a1e673f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutPrivate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostPrivate.java
@@ -22,6 +22,8 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.gerrit.server.update.RetryHelper;
@@ -32,26 +34,30 @@
 import com.google.inject.Singleton;
 
 @Singleton
-public class PutPrivate
-    extends RetryingRestModifyView<ChangeResource, PutPrivate.Input, Response<String>>
+public class PostPrivate
+    extends RetryingRestModifyView<ChangeResource, SetPrivateOp.Input, Response<String>>
     implements UiAction<ChangeResource> {
-  public static class Input {}
-
   private final ChangeMessagesUtil cmUtil;
   private final Provider<ReviewDb> dbProvider;
+  private final PermissionBackend permissionBackend;
 
   @Inject
-  PutPrivate(Provider<ReviewDb> dbProvider, RetryHelper retryHelper, ChangeMessagesUtil cmUtil) {
+  PostPrivate(
+      Provider<ReviewDb> dbProvider,
+      RetryHelper retryHelper,
+      ChangeMessagesUtil cmUtil,
+      PermissionBackend permissionBackend) {
     super(retryHelper);
     this.dbProvider = dbProvider;
     this.cmUtil = cmUtil;
+    this.permissionBackend = permissionBackend;
   }
 
   @Override
-  protected Response<String> applyImpl(
-      BatchUpdate.Factory updateFactory, ChangeResource rsrc, Input input)
+  public Response<String> applyImpl(
+      BatchUpdate.Factory updateFactory, ChangeResource rsrc, SetPrivateOp.Input input)
       throws RestApiException, UpdateException {
-    if (!rsrc.isUserOwner()) {
+    if (!canSetPrivate(rsrc)) {
       throw new AuthException("not allowed to mark private");
     }
 
@@ -60,7 +66,7 @@
     }
 
     ChangeControl control = rsrc.getControl();
-    SetPrivateOp op = new SetPrivateOp(cmUtil, true);
+    SetPrivateOp op = new SetPrivateOp(cmUtil, true, input);
     try (BatchUpdate u =
         updateFactory.create(
             dbProvider.get(),
@@ -82,6 +88,11 @@
         .setVisible(
             !change.isPrivate()
                 && change.getStatus() != Change.Status.MERGED
-                && rsrc.isUserOwner());
+                && canSetPrivate(rsrc));
+  }
+
+  private boolean canSetPrivate(ChangeResource rsrc) {
+    PermissionBackend.WithUser user = permissionBackend.user(rsrc.getUser());
+    return rsrc.isUserOwner() || user.testOrFalse(GlobalPermission.ADMINISTRATE_SERVER);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetPrivateOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetPrivateOp.java
index 1cebcc2..7008eca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SetPrivateOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SetPrivateOp.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.change;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -23,13 +24,25 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.gwtorm.server.OrmException;
 
-class SetPrivateOp implements BatchUpdateOp {
+public class SetPrivateOp implements BatchUpdateOp {
+  public static class Input {
+    String message;
+
+    public Input() {}
+
+    public Input(String message) {
+      this.message = message;
+    }
+  }
+
   private final ChangeMessagesUtil cmUtil;
   private final boolean isPrivate;
+  private final Input input;
 
-  SetPrivateOp(ChangeMessagesUtil cmUtil, boolean isPrivate) {
+  SetPrivateOp(ChangeMessagesUtil cmUtil, boolean isPrivate, Input input) {
     this.cmUtil = cmUtil;
     this.isPrivate = isPrivate;
+    this.input = input;
   }
 
   @Override
@@ -48,10 +61,18 @@
 
   private void addMessage(ChangeContext ctx, ChangeUpdate update) throws OrmException {
     Change c = ctx.getChange();
+    StringBuilder buf = new StringBuilder(c.isPrivate() ? "Set private" : "Unset private");
+
+    String m = Strings.nullToEmpty(input == null ? null : input.message).trim();
+    if (!m.isEmpty()) {
+      buf.append("\n\n");
+      buf.append(m);
+    }
+
     ChangeMessage cmsg =
         ChangeMessagesUtil.newMessage(
             ctx,
-            c.isPrivate() ? "Set private" : "Unset private",
+            buf.toString(),
             c.isPrivate()
                 ? ChangeMessagesUtil.TAG_SET_PRIVATE
                 : ChangeMessagesUtil.TAG_UNSET_PRIVATE);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/WorkInProgressOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/WorkInProgressOp.java
index 7f6e543..21e5dfa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/WorkInProgressOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/WorkInProgressOp.java
@@ -61,7 +61,7 @@
     StringBuilder buf =
         new StringBuilder(c.isWorkInProgress() ? "Set Work In Progress" : "Set Ready For Review");
 
-    String m = Strings.nullToEmpty(in.message).trim();
+    String m = Strings.nullToEmpty(in == null ? null : in.message).trim();
     if (!m.isEmpty()) {
       buf.append("\n\n");
       buf.append(m);
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 8eaa6ec..a8b1837 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
@@ -43,6 +43,7 @@
 import com.google.gerrit.extensions.events.DraftPublishedListener;
 import com.google.gerrit.extensions.events.GarbageCollectorListener;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.GroupIndexedListener;
 import com.google.gerrit.extensions.events.HashtagsEditedListener;
 import com.google.gerrit.extensions.events.HeadUpdatedListener;
 import com.google.gerrit.extensions.events.LifecycleListener;
@@ -328,6 +329,7 @@
     DynamicSet.setOf(binder(), PostUploadHook.class);
     DynamicSet.setOf(binder(), AccountIndexedListener.class);
     DynamicSet.setOf(binder(), ChangeIndexedListener.class);
+    DynamicSet.setOf(binder(), GroupIndexedListener.class);
     DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
     DynamicSet.setOf(binder(), ProjectDeletedListener.class);
     DynamicSet.setOf(binder(), GarbageCollectorListener.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index 6bb9eae..3673101 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -57,6 +57,8 @@
   public final Path ssh_key;
   public final Path ssh_rsa;
   public final Path ssh_dsa;
+  public final Path ssh_ecdsa;
+  public final Path ssh_ed25519;
   public final Path peer_keys;
 
   public final Path site_css;
@@ -98,6 +100,8 @@
     ssh_key = etc_dir.resolve("ssh_host_key");
     ssh_rsa = etc_dir.resolve("ssh_host_rsa_key");
     ssh_dsa = etc_dir.resolve("ssh_host_dsa_key");
+    ssh_ecdsa = etc_dir.resolve("ssh_host_ecdsa_key");
+    ssh_ed25519 = etc_dir.resolve("ssh_host_ed25519_key");
     peer_keys = etc_dir.resolve("peer_keys");
 
     site_css = etc_dir.resolve(CSS_FILENAME);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
index d8a19bf..c086f1c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -17,18 +17,21 @@
 import static com.google.gerrit.reviewdb.client.Change.CHANGE_ID_PATTERN;
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_CONFIG;
 import static com.google.gerrit.server.git.ReceiveCommits.NEW_PATCHSET;
+import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.api.config.ConsistencyCheckInfo.ConsistencyProblemInfo;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.WatchConfig;
+import com.google.gerrit.server.account.externalids.ExternalIdsConsistencyChecker;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -72,6 +75,7 @@
     private final String canonicalWebUrl;
     private final DynamicSet<CommitValidationListener> pluginValidators;
     private final AllUsersName allUsers;
+    private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
     private final String installCommitMsgHookCommand;
 
     @Inject
@@ -80,11 +84,13 @@
         @CanonicalWebUrl @Nullable String canonicalWebUrl,
         @GerritServerConfig Config cfg,
         DynamicSet<CommitValidationListener> pluginValidators,
-        AllUsersName allUsers) {
+        AllUsersName allUsers,
+        ExternalIdsConsistencyChecker externalIdsConsistencyChecker) {
       this.gerritIdent = gerritIdent;
       this.canonicalWebUrl = canonicalWebUrl;
       this.pluginValidators = pluginValidators;
       this.allUsers = allUsers;
+      this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
       this.installCommitMsgHookCommand =
           cfg != null ? cfg.getString("gerrit", null, "installCommitMsgHookCommand") : null;
     }
@@ -104,7 +110,7 @@
               new ConfigValidator(refControl, rw, allUsers),
               new BannedCommitsValidator(rejectCommits),
               new PluginCommitValidationListener(pluginValidators),
-              new BlockExternalIdUpdateListener(allUsers)));
+              new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker)));
     }
 
     public CommitValidators forGerritCommits(RefControl refControl, SshInfo sshInfo, RevWalk rw) {
@@ -118,7 +124,7 @@
                   refControl, canonicalWebUrl, installCommitMsgHookCommand, sshInfo),
               new ConfigValidator(refControl, rw, allUsers),
               new PluginCommitValidationListener(pluginValidators),
-              new BlockExternalIdUpdateListener(allUsers)));
+              new ExternalIdUpdateListener(allUsers, externalIdsConsistencyChecker)));
     }
 
     public CommitValidators forMergedCommits(RefControl refControl) {
@@ -582,11 +588,14 @@
     }
   }
 
-  /** Blocks any update to refs/meta/external-ids */
-  public static class BlockExternalIdUpdateListener implements CommitValidationListener {
+  /** Validates updates to refs/meta/external-ids. */
+  public static class ExternalIdUpdateListener implements CommitValidationListener {
     private final AllUsersName allUsers;
+    private final ExternalIdsConsistencyChecker externalIdsConsistencyChecker;
 
-    public BlockExternalIdUpdateListener(AllUsersName allUsers) {
+    public ExternalIdUpdateListener(
+        AllUsersName allUsers, ExternalIdsConsistencyChecker externalIdsConsistencyChecker) {
+      this.externalIdsConsistencyChecker = externalIdsConsistencyChecker;
       this.allUsers = allUsers;
     }
 
@@ -595,7 +604,26 @@
         throws CommitValidationException {
       if (allUsers.equals(receiveEvent.project.getNameKey())
           && RefNames.REFS_EXTERNAL_IDS.equals(receiveEvent.refName)) {
-        throw new CommitValidationException("not allowed to update " + RefNames.REFS_EXTERNAL_IDS);
+        try {
+          List<ConsistencyProblemInfo> problems =
+              externalIdsConsistencyChecker.check(receiveEvent.commit);
+          List<CommitValidationMessage> msgs =
+              problems
+                  .stream()
+                  .map(
+                      p ->
+                          new CommitValidationMessage(
+                              p.message, p.status == ConsistencyProblemInfo.Status.ERROR))
+                  .collect(toList());
+          if (msgs.stream().anyMatch(m -> m.isError())) {
+            throw new CommitValidationException("invalid external IDs", msgs);
+          }
+          return msgs;
+        } catch (IOException e) {
+          String m = "error validating external IDs";
+          log.warn(m, e);
+          throw new CommitValidationException(m, e);
+        }
       }
       return Collections.emptyList();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
index a9e1362..e1513b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/ChangeIndexRewriter.java
@@ -20,10 +20,12 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.IndexConfig;
 import com.google.gerrit.server.index.IndexPredicate;
 import com.google.gerrit.server.index.IndexRewriter;
 import com.google.gerrit.server.index.QueryOptions;
+import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.query.AndPredicate;
 import com.google.gerrit.server.query.LimitPredicate;
 import com.google.gerrit.server.query.NotPredicate;
@@ -188,6 +190,9 @@
       // and included that in their limit computation.
       return new LimitPredicate<>(ChangeQueryBuilder.FIELD_LIMIT, opts.limit());
     } else if (!isRewritePossible(in)) {
+      if (in instanceof IndexPredicate) {
+        throw new QueryParseException("Unsupported index predicate: " + in.toString());
+      }
       return null; // magic to indicate "in" cannot be rewritten
     }
 
@@ -226,7 +231,10 @@
       return false;
     }
     IndexPredicate<ChangeData> p = (IndexPredicate<ChangeData>) in;
-    return index.getSchema().hasField(p.getField());
+
+    FieldDef<ChangeData, ?> def = p.getField();
+    Schema<ChangeData> schema = index.getSchema();
+    return schema.hasField(def);
   }
 
   private Predicate<ChangeData> partitionChildren(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
index f740f58..b137fb3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/group/GroupIndexerImpl.java
@@ -16,6 +16,8 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.events.GroupIndexedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.index.Index;
@@ -33,19 +35,28 @@
   }
 
   private final GroupCache groupCache;
+  private final DynamicSet<GroupIndexedListener> indexedListener;
   private final GroupIndexCollection indexes;
   private final GroupIndex index;
 
   @AssistedInject
-  GroupIndexerImpl(GroupCache groupCache, @Assisted GroupIndexCollection indexes) {
+  GroupIndexerImpl(
+      GroupCache groupCache,
+      DynamicSet<GroupIndexedListener> indexedListener,
+      @Assisted GroupIndexCollection indexes) {
     this.groupCache = groupCache;
+    this.indexedListener = indexedListener;
     this.indexes = indexes;
     this.index = null;
   }
 
   @AssistedInject
-  GroupIndexerImpl(GroupCache groupCache, @Assisted GroupIndex index) {
+  GroupIndexerImpl(
+      GroupCache groupCache,
+      DynamicSet<GroupIndexedListener> indexedListener,
+      @Assisted GroupIndex index) {
     this.groupCache = groupCache;
+    this.indexedListener = indexedListener;
     this.indexes = null;
     this.index = index;
   }
@@ -55,6 +66,13 @@
     for (Index<?, AccountGroup> i : getWriteIndexes()) {
       i.replace(groupCache.get(uuid));
     }
+    fireGroupIndexedEvent(uuid.get());
+  }
+
+  private void fireGroupIndexedEvent(String uuid) {
+    for (GroupIndexedListener listener : indexedListener) {
+      listener.onGroupIndexed(uuid);
+    }
   }
 
   private Collection<GroupIndex> getWriteIndexes() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java
index 3362897..f350e63 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/ImapMailReceiver.java
@@ -47,7 +47,7 @@
   public synchronized void handleEmails(boolean async) {
     IMAPClient imap;
     if (mailSettings.encryption != Encryption.NONE) {
-      imap = new IMAPSClient(mailSettings.encryption.name(), false);
+      imap = new IMAPSClient(mailSettings.encryption.name(), true);
     } else {
       imap = new IMAPClient();
     }
@@ -71,7 +71,8 @@
           // Fetch just the internal dates first to know how many messages we
           // should fetch.
           if (!imap.fetch("1:*", "(INTERNALDATE)")) {
-            log.error("IMAP fetch failed. Will retry in next fetch cycle.");
+            // false indicates that there are no messages to fetch
+            log.info("Fetched 0 messages via IMAP");
             return;
           }
           // Format of reply is one line per email and one line to indicate
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 606da44..862da9f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -32,12 +32,12 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
 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.ApprovalsUtil;
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
 import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.account.AccountByEmailCache;
+import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.change.EmailReviewComments;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.extensions.events.CommentAdded;
@@ -79,11 +79,11 @@
   private final PatchListCache patchListCache;
   private final PatchSetUtil psUtil;
   private final Provider<InternalChangeQuery> queryProvider;
-  private final Provider<ReviewDb> reviewDb;
   private final DynamicMap<MailFilter> mailFilters;
   private final EmailReviewComments.Factory outgoingMailFactory;
   private final CommentAdded commentAdded;
   private final ApprovalsUtil approvalsUtil;
+  private final AccountCache accountCache;
   private final Provider<String> canonicalUrl;
 
   @Inject
@@ -96,11 +96,11 @@
       PatchListCache patchListCache,
       PatchSetUtil psUtil,
       Provider<InternalChangeQuery> queryProvider,
-      Provider<ReviewDb> reviewDb,
       DynamicMap<MailFilter> mailFilters,
       EmailReviewComments.Factory outgoingMailFactory,
       ApprovalsUtil approvalsUtil,
       CommentAdded commentAdded,
+      AccountCache accountCache,
       @CanonicalWebUrl Provider<String> canonicalUrl) {
     this.accountByEmailCache = accountByEmailCache;
     this.buf = buf;
@@ -110,11 +110,11 @@
     this.patchListCache = patchListCache;
     this.psUtil = psUtil;
     this.queryProvider = queryProvider;
-    this.reviewDb = reviewDb;
     this.mailFilters = mailFilters;
     this.outgoingMailFactory = outgoingMailFactory;
     this.commentAdded = commentAdded;
     this.approvalsUtil = approvalsUtil;
+    this.accountCache = accountCache;
     this.canonicalUrl = canonicalUrl;
   }
 
@@ -153,7 +153,7 @@
       return;
     }
     Account.Id account = accounts.iterator().next();
-    if (!reviewDb.get().accounts().get(account).isActive()) {
+    if (!accountCache.get(account).getAccount().isActive()) {
       log.warn(String.format("Mail: Account %s is inactive. Will delete message.", account));
       return;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java
index e8e2250..d70d651 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java
@@ -49,7 +49,7 @@
   public synchronized void handleEmails(boolean async) {
     POP3Client pop3;
     if (mailSettings.encryption != Encryption.NONE) {
-      pop3 = new POP3SClient(mailSettings.encryption.name());
+      pop3 = new POP3SClient(mailSettings.encryption.name(), true);
     } else {
       pop3 = new POP3Client();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 384daa8..fcde617 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -124,9 +124,10 @@
   }
 
   private final AccountCache accountCache;
+  private final NoteDbUpdateManager.Factory updateManagerFactory;
   private final ChangeDraftUpdate.Factory draftUpdateFactory;
   private final RobotCommentUpdate.Factory robotCommentUpdateFactory;
-  private final NoteDbUpdateManager.Factory updateManagerFactory;
+  private final DeleteCommentRewriter.Factory deleteCommentRewriterFactory;
 
   private final Table<String, Account.Id, Optional<Short>> approvals;
   private final Map<Account.Id, ReviewerStateInternal> reviewers = new LinkedHashMap<>();
@@ -158,6 +159,7 @@
 
   private ChangeDraftUpdate draftUpdate;
   private RobotCommentUpdate robotCommentUpdate;
+  private DeleteCommentRewriter deleteCommentRewriter;
 
   @AssistedInject
   private ChangeUpdate(
@@ -169,6 +171,7 @@
       NoteDbUpdateManager.Factory updateManagerFactory,
       ChangeDraftUpdate.Factory draftUpdateFactory,
       RobotCommentUpdate.Factory robotCommentUpdateFactory,
+      DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
       ProjectCache projectCache,
       @Assisted ChangeControl ctl,
       ChangeNoteUtil noteUtil) {
@@ -181,6 +184,7 @@
         updateManagerFactory,
         draftUpdateFactory,
         robotCommentUpdateFactory,
+        deleteCommentRewriterFactory,
         projectCache,
         ctl,
         serverIdent.getWhen(),
@@ -197,6 +201,7 @@
       NoteDbUpdateManager.Factory updateManagerFactory,
       ChangeDraftUpdate.Factory draftUpdateFactory,
       RobotCommentUpdate.Factory robotCommentUpdateFactory,
+      DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
       ProjectCache projectCache,
       @Assisted ChangeControl ctl,
       @Assisted Date when,
@@ -210,6 +215,7 @@
         updateManagerFactory,
         draftUpdateFactory,
         robotCommentUpdateFactory,
+        deleteCommentRewriterFactory,
         ctl,
         when,
         projectCache.get(getProjectName(ctl)).getLabelTypes().nameComparator(),
@@ -235,15 +241,17 @@
       NoteDbUpdateManager.Factory updateManagerFactory,
       ChangeDraftUpdate.Factory draftUpdateFactory,
       RobotCommentUpdate.Factory robotCommentUpdateFactory,
+      DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
       @Assisted ChangeControl ctl,
       @Assisted Date when,
       @Assisted Comparator<String> labelNameComparator,
       ChangeNoteUtil noteUtil) {
     super(cfg, migration, ctl, serverIdent, anonymousCowardName, noteUtil, when);
     this.accountCache = accountCache;
+    this.updateManagerFactory = updateManagerFactory;
     this.draftUpdateFactory = draftUpdateFactory;
     this.robotCommentUpdateFactory = robotCommentUpdateFactory;
-    this.updateManagerFactory = updateManagerFactory;
+    this.deleteCommentRewriterFactory = deleteCommentRewriterFactory;
     this.approvals = approvals(labelNameComparator);
   }
 
@@ -257,6 +265,7 @@
       NoteDbUpdateManager.Factory updateManagerFactory,
       ChangeDraftUpdate.Factory draftUpdateFactory,
       RobotCommentUpdate.Factory robotCommentUpdateFactory,
+      DeleteCommentRewriter.Factory deleteCommentRewriterFactory,
       ChangeNoteUtil noteUtil,
       @Assisted Change change,
       @Assisted("effective") @Nullable Account.Id accountId,
@@ -280,6 +289,7 @@
     this.draftUpdateFactory = draftUpdateFactory;
     this.robotCommentUpdateFactory = robotCommentUpdateFactory;
     this.updateManagerFactory = updateManagerFactory;
+    this.deleteCommentRewriterFactory = deleteCommentRewriterFactory;
     this.approvals = approvals(labelNameComparator);
   }
 
@@ -394,6 +404,11 @@
     createDraftUpdateIfNull().deleteComment(c);
   }
 
+  public void deleteCommentByRewritingHistory(String uuid, String newMessage) {
+    deleteCommentRewriter =
+        deleteCommentRewriterFactory.create(getChange().getId(), uuid, newMessage);
+  }
+
   @VisibleForTesting
   ChangeDraftUpdate createDraftUpdateIfNull() {
     if (draftUpdate == null) {
@@ -596,6 +611,8 @@
   @Override
   protected CommitBuilder applyImpl(RevWalk rw, ObjectInserter ins, ObjectId curr)
       throws OrmException, IOException {
+    checkState(deleteCommentRewriter == null, "cannot update and rewrite ref in one BatchUpdate");
+
     CommitBuilder cb = new CommitBuilder();
 
     int ps = psId != null ? psId.get() : getChange().currentPatchSetId().get();
@@ -798,6 +815,10 @@
     return robotCommentUpdate;
   }
 
+  public DeleteCommentRewriter getDeleteCommentRewriter() {
+    return deleteCommentRewriter;
+  }
+
   public void setAllowWriteToNewRef(boolean allow) {
     isAllowWriteToNewtRef = allow;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
new file mode 100644
index 0000000..8a43bc6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DeleteCommentRewriter.java
@@ -0,0 +1,246 @@
+// 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.notedb;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.gerrit.reviewdb.client.PatchLineComment.Status.PUBLISHED;
+import static java.util.stream.Collectors.toList;
+import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+/**
+ * Deletes a published comment from NoteDb by rewriting the commit history. Instead of deleting the
+ * whole comment, it just replaces the comment's message with a new message.
+ */
+public class DeleteCommentRewriter implements NoteDbRewriter {
+
+  public interface Factory {
+    /**
+     * Creates a DeleteCommentRewriter instance.
+     *
+     * @param id the id of the change which contains the target comment.
+     * @param uuid the uuid of the target comment.
+     * @param newMessage the message used to replace the old message of the target comment.
+     * @return the DeleteCommentRewriter instance
+     */
+    DeleteCommentRewriter create(
+        Change.Id id, @Assisted("uuid") String uuid, @Assisted("newMessage") String newMessage);
+  }
+
+  private final ChangeNoteUtil noteUtil;
+  private final Change.Id changeId;
+  private final String uuid;
+  private final String newMessage;
+
+  @Inject
+  DeleteCommentRewriter(
+      ChangeNoteUtil noteUtil,
+      @Assisted Change.Id changeId,
+      @Assisted("uuid") String uuid,
+      @Assisted("newMessage") String newMessage) {
+    this.noteUtil = noteUtil;
+    this.changeId = changeId;
+    this.uuid = uuid;
+    this.newMessage = newMessage;
+  }
+
+  @Override
+  public String getRefName() {
+    return RefNames.changeMetaRef(changeId);
+  }
+
+  @Override
+  public ObjectId rewriteCommitHistory(RevWalk revWalk, ObjectInserter inserter, ObjectId currTip)
+      throws IOException, ConfigInvalidException, OrmException {
+    checkArgument(!currTip.equals(ObjectId.zeroId()));
+
+    // Walk from the first commit of the branch.
+    revWalk.reset();
+    revWalk.markStart(revWalk.parseCommit(currTip));
+    revWalk.sort(RevSort.REVERSE);
+
+    ObjectReader reader = revWalk.getObjectReader();
+    ObjectId newTip = revWalk.next(); // The first commit will not be rewritten.
+    NoteMap newTipNoteMap = NoteMap.read(reader, revWalk.parseCommit(newTip));
+    Map<String, Comment> parentComments =
+        getPublishedComments(noteUtil, changeId, reader, newTipNoteMap);
+
+    boolean rewrite = false;
+    RevCommit originalCommit;
+    while ((originalCommit = revWalk.next()) != null) {
+      NoteMap noteMap = NoteMap.read(reader, originalCommit);
+      Map<String, Comment> currComments = getPublishedComments(noteUtil, changeId, reader, noteMap);
+
+      if (!rewrite && currComments.containsKey(uuid)) {
+        rewrite = true;
+      }
+
+      if (!rewrite) {
+        parentComments = currComments;
+        newTip = originalCommit;
+        continue;
+      }
+
+      List<Comment> putInComments = getPutInComments(parentComments, currComments);
+      List<Comment> deletedComments = getDeletedComments(parentComments, currComments);
+      newTip =
+          rewriteCommit(
+              originalCommit,
+              newTipNoteMap,
+              newTip,
+              inserter,
+              reader,
+              putInComments,
+              deletedComments);
+      newTipNoteMap = NoteMap.read(reader, revWalk.parseCommit(newTip));
+      parentComments = currComments;
+    }
+
+    return newTip;
+  }
+
+  /**
+   * Gets all the comments which are presented at a commit. Note they include the comments put in by
+   * the previous commits.
+   */
+  @VisibleForTesting
+  public static Map<String, Comment> getPublishedComments(
+      ChangeNoteUtil noteUtil, Change.Id changeId, ObjectReader reader, NoteMap noteMap)
+      throws IOException, ConfigInvalidException {
+    return RevisionNoteMap.parse(noteUtil, changeId, reader, noteMap, PUBLISHED)
+        .revisionNotes
+        .values()
+        .stream()
+        .flatMap(n -> n.getComments().stream())
+        .collect(Collectors.toMap(c -> c.key.uuid, c -> c));
+  }
+
+  /**
+   * Gets the comments put in by the current commit. The message of the target comment will be
+   * replaced by the new message.
+   *
+   * @param parMap the comment map of the parent commit.
+   * @param curMap the comment map of the current commit.
+   * @return The comments put in by the current commit.
+   */
+  private List<Comment> getPutInComments(Map<String, Comment> parMap, Map<String, Comment> curMap) {
+    List<Comment> comments = new ArrayList<>();
+    for (String key : curMap.keySet()) {
+      if (!parMap.containsKey(key)) {
+        Comment comment = curMap.get(key);
+        if (key.equals(uuid)) {
+          comment.message = newMessage;
+        }
+        comments.add(comment);
+      }
+    }
+    return comments;
+  }
+
+  /**
+   * Gets the comments deleted by the current commit.
+   *
+   * @param parMap the comment map of the parent commit.
+   * @param curMap the comment map of the current commit.
+   * @return The comments deleted by the current commit.
+   */
+  private List<Comment> getDeletedComments(
+      Map<String, Comment> parMap, Map<String, Comment> curMap) {
+    return parMap
+        .entrySet()
+        .stream()
+        .filter(c -> !curMap.containsKey(c.getKey()))
+        .map(c -> c.getValue())
+        .collect(toList());
+  }
+
+  /**
+   * Rewrites one commit.
+   *
+   * @param originalCommit the original commit to be rewritten.
+   * @param parentNoteMap the {@code NoteMap} of the new commit's parent.
+   * @param parentId the {@code ObjectId} of the new commit's parent.
+   * @param inserter the {@code ObjectInserter} for the rewrite process.
+   * @param reader the {@code ObjectReader} for the rewrite process.
+   * @param putInComments the comments put in by this commit.
+   * @param deletedComments the comments deleted by this commit.
+   * @return the {@code objectId} of the new commit.
+   * @throws IOException
+   * @throws ConfigInvalidException
+   */
+  private ObjectId rewriteCommit(
+      RevCommit originalCommit,
+      NoteMap parentNoteMap,
+      ObjectId parentId,
+      ObjectInserter inserter,
+      ObjectReader reader,
+      List<Comment> putInComments,
+      List<Comment> deletedComments)
+      throws IOException, ConfigInvalidException {
+    RevisionNoteMap<ChangeRevisionNote> revNotesMap =
+        RevisionNoteMap.parse(noteUtil, changeId, reader, parentNoteMap, PUBLISHED);
+    RevisionNoteBuilder.Cache cache = new RevisionNoteBuilder.Cache(revNotesMap);
+
+    for (Comment c : putInComments) {
+      cache.get(new RevId(c.revId)).putComment(c);
+    }
+
+    for (Comment c : deletedComments) {
+      cache.get(new RevId(c.revId)).deleteComment(c.key);
+    }
+
+    Map<RevId, RevisionNoteBuilder> builders = cache.getBuilders();
+    for (Map.Entry<RevId, RevisionNoteBuilder> entry : builders.entrySet()) {
+      ObjectId objectId = ObjectId.fromString(entry.getKey().get());
+      byte[] data = entry.getValue().build(noteUtil, noteUtil.getWriteJson());
+      if (data.length == 0) {
+        revNotesMap.noteMap.remove(objectId);
+      }
+      revNotesMap.noteMap.set(objectId, inserter.insert(OBJ_BLOB, data));
+    }
+
+    CommitBuilder cb = new CommitBuilder();
+    cb.setParentId(parentId);
+    cb.setTreeId(revNotesMap.noteMap.writeTree(inserter));
+    cb.setMessage(originalCommit.getFullMessage());
+    cb.setCommitter(originalCommit.getCommitterIdent());
+    cb.setAuthor(originalCommit.getAuthorIdent());
+    cb.setEncoding(originalCommit.getEncoding());
+
+    return inserter.insert(cb);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
index d249689..64b8b44 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
@@ -49,6 +49,7 @@
   public void configure() {
     factory(ChangeUpdate.Factory.class);
     factory(ChangeDraftUpdate.Factory.class);
+    factory(DeleteCommentRewriter.Factory.class);
     factory(DraftCommentNotes.Factory.class);
     factory(RobotCommentUpdate.Factory.class);
     factory(RobotCommentNotes.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbRewriter.java
new file mode 100644
index 0000000..3c7b0a3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbRewriter.java
@@ -0,0 +1,39 @@
+// 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.notedb;
+
+import com.google.gwtorm.server.OrmException;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+public interface NoteDbRewriter {
+
+  /** Gets the name of the target ref which will be rewritten. */
+  String getRefName();
+
+  /**
+   * Rewrites the commit history.
+   *
+   * @param revWalk a {@code RevWalk} instance.
+   * @param inserter a {@code ObjectInserter} instance.
+   * @param currTip the {@code ObjectId} of the ref's tip commit.
+   * @return the {@code ObjectId} of the ref's new tip commit.
+   */
+  ObjectId rewriteCommitHistory(RevWalk revWalk, ObjectInserter inserter, ObjectId currTip)
+      throws IOException, ConfigInvalidException, OrmException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index 3aa2748..6b3492a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -54,6 +54,7 @@
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.NullProgressMonitor;
@@ -205,6 +206,7 @@
   private final ListMultimap<String, ChangeUpdate> changeUpdates;
   private final ListMultimap<String, ChangeDraftUpdate> draftUpdates;
   private final ListMultimap<String, RobotCommentUpdate> robotCommentUpdates;
+  private final ListMultimap<String, NoteDbRewriter> rewriters;
   private final Set<Change.Id> toDelete;
 
   private OpenRepo changeRepo;
@@ -232,6 +234,7 @@
     changeUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
     draftUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
     robotCommentUpdates = MultimapBuilder.hashKeys().arrayListValues().build();
+    rewriters = MultimapBuilder.hashKeys().arrayListValues().build();
     toDelete = new HashSet<>();
   }
 
@@ -344,6 +347,7 @@
     return changeUpdates.isEmpty()
         && draftUpdates.isEmpty()
         && robotCommentUpdates.isEmpty()
+        && rewriters.isEmpty()
         && toDelete.isEmpty()
         && !hasCommands(changeRepo)
         && !hasCommands(allUsersRepo);
@@ -377,6 +381,10 @@
     if (rcu != null) {
       robotCommentUpdates.put(rcu.getRefName(), rcu);
     }
+    DeleteCommentRewriter deleteCommentRewriter = update.getDeleteCommentRewriter();
+    if (deleteCommentRewriter != null) {
+      rewriters.put(deleteCommentRewriter.getRefName(), deleteCommentRewriter);
+    }
   }
 
   public void add(ChangeDraftUpdate draftUpdate) {
@@ -603,6 +611,21 @@
     if (!robotCommentUpdates.isEmpty()) {
       addUpdates(robotCommentUpdates, changeRepo);
     }
+    if (!rewriters.isEmpty()) {
+      Optional<String> conflictKey =
+          rewriters
+              .keySet()
+              .stream()
+              .filter(k -> (draftUpdates.containsKey(k) || robotCommentUpdates.containsKey(k)))
+              .findAny();
+      if (conflictKey.isPresent()) {
+        throw new IllegalArgumentException(
+            String.format(
+                "cannot update and rewrite ref %s in one BatchUpdate", conflictKey.get()));
+      }
+      addRewrites(rewriters, changeRepo);
+    }
+
     for (Change.Id id : toDelete) {
       doDelete(id);
     }
@@ -723,6 +746,35 @@
     }
   }
 
+  private static void addRewrites(ListMultimap<String, NoteDbRewriter> rewriters, OpenRepo openRepo)
+      throws OrmException, IOException {
+    for (Map.Entry<String, Collection<NoteDbRewriter>> entry : rewriters.asMap().entrySet()) {
+      String refName = entry.getKey();
+      ObjectId oldTip = openRepo.cmds.get(refName).orElse(ObjectId.zeroId());
+
+      if (oldTip.equals(ObjectId.zeroId())) {
+        throw new OrmException(String.format("Ref %s is empty", refName));
+      }
+
+      ObjectId currTip = oldTip;
+      try {
+        for (NoteDbRewriter noteDbRewriter : entry.getValue()) {
+          ObjectId nextTip =
+              noteDbRewriter.rewriteCommitHistory(openRepo.rw, openRepo.tempIns, currTip);
+          if (nextTip != null) {
+            currTip = nextTip;
+          }
+        }
+      } catch (ConfigInvalidException e) {
+        throw new OrmException("Cannot rewrite commit history", e);
+      }
+
+      if (!oldTip.equals(currTip)) {
+        openRepo.cmds.add(new ReceiveCommand(oldTip, currTip, refName));
+      }
+    }
+  }
+
   private static <U extends AbstractChangeUpdate> boolean allowWrite(
       Collection<U> updates, ObjectId old) {
     if (!old.equals(ObjectId.zeroId())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 8fabe44..16c63c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -286,6 +286,7 @@
   /** Can this user rebase this change? */
   private boolean canRebase(ReviewDb db) throws OrmException {
     return (isOwner() || getRefControl().canSubmit(isOwner()) || getRefControl().canRebase())
+        && getRefControl().canUpload()
         && !isPatchSetLocked(db);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
index c8857eb..8891af5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/account/AccountQueryBuilder.java
@@ -124,7 +124,7 @@
   @Override
   protected Predicate<AccountState> defaultField(String query) {
     Predicate<AccountState> defaultPredicate = AccountPredicates.defaultPredicate(query);
-    if ("self".equalsIgnoreCase(query)) {
+    if ("self".equalsIgnoreCase(query) || "me".equalsIgnoreCase(query)) {
       try {
         return Predicate.or(defaultPredicate, AccountPredicates.id(self()));
       } catch (QueryParseException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 099a3d1..611a1a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -118,6 +118,8 @@
   private static final Pattern DEF_CHANGE =
       Pattern.compile("^(?:[1-9][0-9]*|(?:[^~]+~[^~]+~)?[iI][0-9a-f]{4,}.*)$");
 
+  private static final int MAX_ACCOUNTS_PER_DEFAULT_FIELD = 10;
+
   // NOTE: As new search operations are added, please keep the
   // SearchSuggestOracle up to date.
 
@@ -884,9 +886,13 @@
     return new HasDraftByPredicate(who);
   }
 
+  private boolean isSelf(String who) {
+    return "self".equals(who) || "me".equals(who);
+  }
+
   @Operator
   public Predicate<ChangeData> visibleto(String who) throws QueryParseException, OrmException {
-    if ("self".equals(who)) {
+    if (isSelf(who)) {
       return is_visible();
     }
     Set<Account.Id> m = args.accountResolver.findAll(args.db.get(), who);
@@ -939,6 +945,15 @@
     return Predicate.or(p);
   }
 
+  private Predicate<ChangeData> ownerDefaultField(String who)
+      throws QueryParseException, OrmException {
+    Set<Account.Id> accounts = parseAccount(who);
+    if (accounts.size() > MAX_ACCOUNTS_PER_DEFAULT_FIELD) {
+      return Predicate.any();
+    }
+    return owner(accounts);
+  }
+
   @Operator
   public Predicate<ChangeData> assignee(String who) throws QueryParseException, OrmException {
     return assignee(parseAccount(who));
@@ -968,17 +983,27 @@
 
   @Operator
   public Predicate<ChangeData> reviewer(String who) throws QueryParseException, OrmException {
+    return reviewer(who, false);
+  }
+
+  private Predicate<ChangeData> reviewerDefaultField(String who)
+      throws QueryParseException, OrmException {
+    return reviewer(who, true);
+  }
+
+  private Predicate<ChangeData> reviewer(String who, boolean forDefaultField)
+      throws QueryParseException, OrmException {
     if (args.getSchema().hasField(ChangeField.WIP)) {
       return Predicate.and(
           Predicate.not(new BooleanPredicate(ChangeField.WIP, args.fillArgs)),
-          reviewerByState(who, ReviewerStateInternal.REVIEWER));
+          reviewerByState(who, ReviewerStateInternal.REVIEWER, forDefaultField));
     }
-    return reviewerByState(who, ReviewerStateInternal.REVIEWER);
+    return reviewerByState(who, ReviewerStateInternal.REVIEWER, forDefaultField);
   }
 
   @Operator
   public Predicate<ChangeData> cc(String who) throws QueryParseException, OrmException {
-    return reviewerByState(who, ReviewerStateInternal.CC);
+    return reviewerByState(who, ReviewerStateInternal.CC, false);
   }
 
   @Operator
@@ -1137,12 +1162,12 @@
     // Adapt the capacity of this list when adding more default predicates.
     List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(11);
     try {
-      predicates.add(owner(query));
+      predicates.add(ownerDefaultField(query));
     } catch (OrmException | QueryParseException e) {
       // Skip.
     }
     try {
-      predicates.add(reviewer(query));
+      predicates.add(reviewerDefaultField(query));
     } catch (OrmException | QueryParseException e) {
       // Skip.
     }
@@ -1165,7 +1190,7 @@
   }
 
   private Set<Account.Id> parseAccount(String who) throws QueryParseException, OrmException {
-    if ("self".equals(who)) {
+    if (isSelf(who)) {
       return Collections.singleton(self());
     }
     Set<Account.Id> matches = args.accountResolver.findAll(args.db.get(), who);
@@ -1208,7 +1233,8 @@
     return args.getIdentifiedUser().getAccountId();
   }
 
-  public Predicate<ChangeData> reviewerByState(String who, ReviewerStateInternal state)
+  public Predicate<ChangeData> reviewerByState(
+      String who, ReviewerStateInternal state, boolean forDefaultField)
       throws QueryParseException, OrmException {
     Predicate<ChangeData> reviewerByEmailPredicate = null;
     if (args.index.getSchema().hasField(ChangeField.REVIEWER_BY_EMAIL)) {
@@ -1220,12 +1246,15 @@
 
     Predicate<ChangeData> reviewerPredicate = null;
     try {
-      reviewerPredicate =
-          Predicate.or(
-              parseAccount(who)
-                  .stream()
-                  .map(id -> ReviewerPredicate.forState(args, id, state))
-                  .collect(toList()));
+      Set<Account.Id> accounts = parseAccount(who);
+      if (!forDefaultField || accounts.size() <= MAX_ACCOUNTS_PER_DEFAULT_FIELD) {
+        reviewerPredicate =
+            Predicate.or(
+                accounts
+                    .stream()
+                    .map(id -> ReviewerPredicate.forState(args, id, state))
+                    .collect(toList()));
+      }
     } catch (QueryParseException e) {
       // Propagate this exception only if we can't use 'who' to query by email
       if (reviewerByEmailPredicate == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index be41a0a..76decec 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -35,7 +35,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_149> C = Schema_149.class;
+  public static final Class<Schema_150> C = Schema_150.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_142.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_142.java
index df808df..e67ae2f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_142.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_142.java
@@ -14,16 +14,24 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.client.AccountExternalId;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+
+import com.google.common.base.Strings;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.HashedPassword;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.util.List;
+import java.sql.Statement;
 
 public class Schema_142 extends SchemaVersion {
+  private static final int MAX_BATCH_SIZE = 1000;
+
   @Inject
   Schema_142(Provider<Schema_141> prior) {
     super(prior);
@@ -31,19 +39,39 @@
 
   @Override
   protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    List<AccountExternalId> newIds = db.accountExternalIds().all().toList();
-    for (AccountExternalId id : newIds) {
-      if (!id.isScheme(AccountExternalId.SCHEME_USERNAME)) {
-        continue;
+    try (PreparedStatement updateStmt =
+        ((JdbcSchema) db)
+            .getConnection()
+            .prepareStatement(
+                "UPDATE account_external_ids " + "SET password = ? " + "WHERE external_id = ?")) {
+      int batchCount = 0;
+
+      try (Statement stmt = newStatement(db);
+          ResultSet rs =
+              stmt.executeQuery("SELECT external_id, password FROM account_external_ids")) {
+        while (rs.next()) {
+          String externalId = rs.getString("external_id");
+          String password = rs.getString("password");
+          if (!ExternalId.Key.parse(externalId).isScheme(SCHEME_USERNAME)
+              || Strings.isNullOrEmpty(password)) {
+            continue;
+          }
+
+          HashedPassword hashed = HashedPassword.fromPassword(password);
+          updateStmt.setString(1, hashed.encode());
+          updateStmt.setString(2, externalId);
+          updateStmt.addBatch();
+          batchCount++;
+          if (batchCount >= MAX_BATCH_SIZE) {
+            updateStmt.executeBatch();
+            batchCount = 0;
+          }
+        }
       }
 
-      String password = id.getPassword();
-      if (password != null) {
-        HashedPassword hashed = HashedPassword.fromPassword(password);
-        id.setPassword(hashed.encode());
+      if (batchCount > 0) {
+        updateStmt.executeBatch();
       }
     }
-
-    db.accountExternalIds().upsert(newIds);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_144.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_144.java
index 70e55cf..eaa97e4d5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_144.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_144.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -21,11 +22,15 @@
 import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
+import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.HashSet;
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.ObjectId;
@@ -56,7 +61,26 @@
 
   @Override
   protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    Set<ExternalId> toAdd = ExternalId.from(db.accountExternalIds().all().toList());
+    Set<ExternalId> toAdd = new HashSet<>();
+    try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+        ResultSet rs =
+            stmt.executeQuery(
+                "SELECT "
+                    + "account_id, "
+                    + "email_address, "
+                    + "password, "
+                    + "external_id "
+                    + "FROM account_external_ids")) {
+      while (rs.next()) {
+        Account.Id accountId = new Account.Id(rs.getInt(1));
+        String email = rs.getString(2);
+        String password = rs.getString(3);
+        String externalId = rs.getString(4);
+
+        toAdd.add(ExternalId.create(ExternalId.Key.parse(externalId), accountId, email, password));
+      }
+    }
+
     try {
       try (Repository repo = repoManager.openRepository(allUsersName);
           RevWalk rw = new RevWalk(repo);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_150.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_150.java
new file mode 100644
index 0000000..4830fe1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_150.java
@@ -0,0 +1,26 @@
+// 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.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/** Drop ACCOUNT_EXTERNAL_IDS table. */
+public class Schema_150 extends SchemaVersion {
+  @Inject
+  Schema_150(Provider<Schema_149> prior) {
+    super(prior);
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
index cb1d97b..b80e31e 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/change/ChangeIndexRewriterTest.java
@@ -29,7 +29,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.index.IndexConfig;
 import com.google.gerrit.server.index.QueryOptions;
-import com.google.gerrit.server.query.AndPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.AndChangeSource;
@@ -196,9 +195,10 @@
     assertThat(rewrite(in)).isEqualTo(query(in));
 
     indexes.setSearchIndex(new FakeChangeIndex(FakeChangeIndex.V1));
-    Predicate<ChangeData> out = rewrite(in);
-    assertThat(out).isInstanceOf(AndPredicate.class);
-    assertThat(out.getChildren()).containsExactly(query(in.getChild(0)), in.getChild(1)).inOrder();
+
+    exception.expect(QueryParseException.class);
+    exception.expectMessage("Unsupported index predicate: file:a");
+    rewrite(in);
   }
 
   @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index 3d13536..ee894c9 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -98,6 +98,7 @@
   @Inject protected AllProjectsName allProjects;
 
   protected LifecycleManager lifecycle;
+  protected Injector injector;
   protected ReviewDb db;
   protected AccountInfo currentUserInfo;
   protected CurrentUser user;
@@ -107,11 +108,14 @@
   @Before
   public void setUpInjector() throws Exception {
     lifecycle = new LifecycleManager();
-    Injector injector = createInjector();
+    injector = createInjector();
     lifecycle.add(injector);
     injector.injectMembers(this);
     lifecycle.start();
+    setUpDatabase();
+  }
 
+  protected void setUpDatabase() throws Exception {
     db = schemaFactory.open();
     schemaCreator.create(db);
 
@@ -255,6 +259,7 @@
     assertQuery("Jo Do", user1);
     assertQuery("jo do", user1);
     assertQuery("self", currentUserInfo, user3);
+    assertQuery("me", currentUserInfo);
     assertQuery("name:John", user1);
     assertQuery("name:john", user1);
     assertQuery("name:Doe", user1);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 34ef0b5..0b01362 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -399,7 +399,7 @@
     assertQuery("is:open", change2, change1);
     assertQuery("is:private");
 
-    gApi.changes().id(change1.getChangeId()).setPrivate(true);
+    gApi.changes().id(change1.getChangeId()).setPrivate(true, null);
 
     // Change1 is not private, but should be still visible to its owner.
     assertQuery("is:open", change1, change2);
@@ -1988,6 +1988,19 @@
         .containsExactlyElementsIn(expectedPatterns);
   }
 
+  @Test
+  public void selfAndMe() throws Exception {
+    TestRepository<Repo> repo = createProject("repo");
+    Change change1 = insert(repo, newChange(repo));
+    Change change2 = insert(repo, newChange(repo), userId);
+    insert(repo, newChange(repo));
+    gApi.accounts().self().starChange(change1.getId().toString());
+    gApi.accounts().self().starChange(change2.getId().toString());
+
+    assertQuery("starredby:self", change2, change1);
+    assertQuery("starredby:me", change2, change1);
+  }
+
   protected ChangeInserter newChange(TestRepository<Repo> repo) throws Exception {
     return newChange(repo, null, null, null, null);
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index a0e5ee0..4b8309a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -98,6 +98,7 @@
   @Inject protected GroupCache groupCache;
 
   protected LifecycleManager lifecycle;
+  protected Injector injector;
   protected ReviewDb db;
   protected AccountInfo currentUserInfo;
   protected CurrentUser user;
@@ -107,11 +108,14 @@
   @Before
   public void setUpInjector() throws Exception {
     lifecycle = new LifecycleManager();
-    Injector injector = createInjector();
+    injector = createInjector();
     lifecycle.add(injector);
     injector.injectMembers(this);
     lifecycle.start();
+    setUpDatabase();
+  }
 
+  protected void setUpDatabase() throws Exception {
     db = schemaFactory.open();
     schemaCreator.create(db);
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
index 21c8764..3d4a1a0 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/HookTestCase.java
@@ -54,10 +54,11 @@
 
 import com.google.common.io.ByteStreams;
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.net.URL;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -119,7 +120,7 @@
       try (InputStream in = url.openStream()) {
         hook = File.createTempFile("hook_", ".sh");
         cleanup.add(hook);
-        try (FileOutputStream out = new FileOutputStream(hook)) {
+        try (OutputStream out = Files.newOutputStream(hook.toPath())) {
           ByteStreams.copy(in, out);
         }
       }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
index 885a1f5..870dba7 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/DisabledReviewDb.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.testutil;
 
 import com.google.gerrit.reviewdb.server.AccountAccess;
-import com.google.gerrit.reviewdb.server.AccountExternalIdAccess;
 import com.google.gerrit.reviewdb.server.AccountGroupAccess;
 import com.google.gerrit.reviewdb.server.AccountGroupByIdAccess;
 import com.google.gerrit.reviewdb.server.AccountGroupByIdAudAccess;
@@ -89,11 +88,6 @@
   }
 
   @Override
-  public AccountExternalIdAccess accountExternalIds() {
-    throw new Disabled();
-  }
-
-  @Override
   public AccountGroupAccess accountGroups() {
     throw new Disabled();
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index 5f425d1..d655500 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -81,7 +81,11 @@
   }
 
   private static Set<PublicKey> myHostKeys(KeyPairProvider p) {
-    final Set<PublicKey> keys = new HashSet<>(2);
+    final Set<PublicKey> keys = new HashSet<>(6);
+    addPublicKey(keys, p, KeyPairProvider.SSH_ED25519);
+    addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP256);
+    addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP384);
+    addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP521);
     addPublicKey(keys, p, KeyPairProvider.SSH_RSA);
     addPublicKey(keys, p, KeyPairProvider.SSH_DSS);
     return keys;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java
index 589014c..8764357 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/HostKeyProvider.java
@@ -25,7 +25,6 @@
 import java.util.List;
 import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
-import org.apache.sshd.common.util.security.SecurityUtils;
 import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
 
 class HostKeyProvider implements Provider<KeyPairProvider> {
@@ -41,14 +40,22 @@
     Path objKey = site.ssh_key;
     Path rsaKey = site.ssh_rsa;
     Path dsaKey = site.ssh_dsa;
+    Path ecdsaKey = site.ssh_ecdsa;
+    Path ed25519Key = site.ssh_ed25519;
 
-    final List<File> stdKeys = new ArrayList<>(2);
+    final List<File> stdKeys = new ArrayList<>(4);
     if (Files.exists(rsaKey)) {
       stdKeys.add(rsaKey.toAbsolutePath().toFile());
     }
     if (Files.exists(dsaKey)) {
       stdKeys.add(dsaKey.toAbsolutePath().toFile());
     }
+    if (Files.exists(ecdsaKey)) {
+      stdKeys.add(ecdsaKey.toAbsolutePath().toFile());
+    }
+    if (Files.exists(ed25519Key)) {
+      stdKeys.add(ed25519Key.toAbsolutePath().toFile());
+    }
 
     if (Files.exists(objKey)) {
       if (stdKeys.isEmpty()) {
@@ -65,13 +72,6 @@
     if (stdKeys.isEmpty()) {
       throw new ProvisionException("No SSH keys under " + site.etc_dir);
     }
-    if (!SecurityUtils.isBouncyCastleRegistered()) {
-      throw new ProvisionException(
-          "Bouncy Castle Crypto not installed;"
-              + " needed to read server host keys: "
-              + stdKeys
-              + "");
-    }
     FileKeyPairProvider kp = new FileKeyPairProvider();
     kp.setFiles(stdKeys);
     return kp;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index 45f68e8..3c108b0 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
@@ -84,7 +84,6 @@
 import org.apache.sshd.common.kex.KeyExchange;
 import org.apache.sshd.common.keyprovider.KeyPairProvider;
 import org.apache.sshd.common.mac.Mac;
-import org.apache.sshd.common.random.JceRandomFactory;
 import org.apache.sshd.common.random.Random;
 import org.apache.sshd.common.random.SingletonRandomFactory;
 import org.apache.sshd.common.session.ConnectionService;
@@ -189,6 +188,7 @@
 
     long idleTimeoutSeconds = ConfigUtil.getTimeUnit(cfg, "sshd", null, "idleTimeout", 0, SECONDS);
     getProperties().put(IDLE_TIMEOUT, String.valueOf(SECONDS.toMillis(idleTimeoutSeconds)));
+    getProperties().put(NIO2_READ_TIMEOUT, String.valueOf(SECONDS.toMillis(idleTimeoutSeconds)));
 
     long rekeyTimeLimit =
         ConfigUtil.getTimeUnit(cfg, "sshd", null, "rekeyTimeLimit", 3600, SECONDS);
@@ -217,11 +217,7 @@
             ? MinaServiceFactoryFactory.class.getName()
             : Nio2ServiceFactoryFactory.class.getName());
 
-    if (SecurityUtils.isBouncyCastleRegistered()) {
-      initProviderBouncyCastle(cfg);
-    } else {
-      initProviderJce();
-    }
+    initProviderBouncyCastle(cfg);
     initCiphers(cfg);
     initKeyExchanges(cfg);
     initMacs(cfg);
@@ -405,7 +401,9 @@
         try {
           r.add(new HostKey(addr, keyBin));
         } catch (JSchException e) {
-          sshDaemonLog.warn("Cannot format SSHD host key", e);
+          sshDaemonLog.warn(
+              String.format(
+                  "Cannot format SSHD host key [%s]: %s", pub.getAlgorithm(), e.getMessage()));
         }
       }
     }
@@ -414,7 +412,11 @@
 
   private List<PublicKey> myHostKeys() {
     final KeyPairProvider p = getKeyPairProvider();
-    final List<PublicKey> keys = new ArrayList<>(2);
+    final List<PublicKey> keys = new ArrayList<>(6);
+    addPublicKey(keys, p, KeyPairProvider.SSH_ED25519);
+    addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP256);
+    addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP384);
+    addPublicKey(keys, p, KeyPairProvider.ECDSA_SHA2_NISTP521);
     addPublicKey(keys, p, KeyPairProvider.SSH_RSA);
     addPublicKey(keys, p, KeyPairProvider.SSH_DSS);
     return keys;
@@ -520,10 +522,6 @@
     }
   }
 
-  private void initProviderJce() {
-    setRandomFactory(new SingletonRandomFactory(JceRandomFactory.INSTANCE));
-  }
-
   @SuppressWarnings("unchecked")
   private void initCiphers(final Config cfg) {
     final List<NamedFactory<Cipher>> a = BaseBuilder.setUpDefaultCiphers(true);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index a3cf7c1..6a68211 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -19,14 +19,12 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.VersionedAuthorizedKeys;
 import com.google.gerrit.server.account.externalids.ExternalId;
 import com.google.gerrit.server.account.externalids.ExternalIds;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gerrit.server.ssh.SshKeyCreator;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
@@ -92,40 +90,33 @@
   }
 
   static class Loader extends CacheLoader<String, Iterable<SshKeyCacheEntry>> {
-    private final SchemaFactory<ReviewDb> schema;
     private final ExternalIds externalIds;
     private final VersionedAuthorizedKeys.Accessor authorizedKeys;
 
     @Inject
-    Loader(
-        SchemaFactory<ReviewDb> schema,
-        ExternalIds externalIds,
-        VersionedAuthorizedKeys.Accessor authorizedKeys) {
-      this.schema = schema;
+    Loader(ExternalIds externalIds, VersionedAuthorizedKeys.Accessor authorizedKeys) {
       this.externalIds = externalIds;
       this.authorizedKeys = authorizedKeys;
     }
 
     @Override
     public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
-      try (ReviewDb db = schema.open()) {
-        ExternalId user = externalIds.get(db, ExternalId.Key.create(SCHEME_USERNAME, username));
-        if (user == null) {
-          return NO_SUCH_USER;
-        }
-
-        List<SshKeyCacheEntry> kl = new ArrayList<>(4);
-        for (AccountSshKey k : authorizedKeys.getKeys(user.accountId())) {
-          if (k.isValid()) {
-            add(kl, k);
-          }
-        }
-
-        if (kl.isEmpty()) {
-          return NO_KEYS;
-        }
-        return Collections.unmodifiableList(kl);
+      ExternalId user = externalIds.get(ExternalId.Key.create(SCHEME_USERNAME, username));
+      if (user == null) {
+        return NO_SUCH_USER;
       }
+
+      List<SshKeyCacheEntry> kl = new ArrayList<>(4);
+      for (AccountSshKey k : authorizedKeys.getKeys(user.accountId())) {
+        if (k.isValid()) {
+          add(kl, k);
+        }
+      }
+
+      if (kl.isEmpty()) {
+        return NO_KEYS;
+      }
+      return Collections.unmodifiableList(kl);
     }
 
     private void add(List<SshKeyCacheEntry> kl, AccountSshKey k) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
index 5a47cb0..820052c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
@@ -25,12 +25,11 @@
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.nio.file.Files;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -54,6 +53,7 @@
 
   @Inject private PluginLoader loader;
 
+  @SuppressWarnings("resource")
   @Override
   protected void run() throws UnloggedFailure {
     if (!loader.isRemoteAdminEnabled()) {
@@ -80,8 +80,8 @@
       data = in;
     } else if (new File(source).isFile() && source.equals(new File(source).getAbsolutePath())) {
       try {
-        data = new FileInputStream(new File(source));
-      } catch (FileNotFoundException e) {
+        data = Files.newInputStream(new File(source).toPath());
+      } catch (IOException e) {
         throw die("cannot read " + source);
       }
     } else {
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/UnzippedDistribution.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/UnzippedDistribution.java
index b5a1dae..ec92fba 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/UnzippedDistribution.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/UnzippedDistribution.java
@@ -20,10 +20,10 @@
 import com.google.gerrit.pgm.init.PluginsDistribution;
 import com.google.inject.Singleton;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.List;
 import javax.servlet.ServletContext;
@@ -45,7 +45,7 @@
       for (File p : list) {
         String pluginJarName = p.getName();
         String pluginName = pluginJarName.substring(0, pluginJarName.length() - JAR.length());
-        try (InputStream in = new FileInputStream(p)) {
+        try (InputStream in = Files.newInputStream(p.toPath())) {
           processor.process(pluginName, in);
         }
       }
diff --git a/lib/asciidoctor/java/AsciiDoctor.java b/lib/asciidoctor/java/AsciiDoctor.java
index d765cc1..219cc24 100644
--- a/lib/asciidoctor/java/AsciiDoctor.java
+++ b/lib/asciidoctor/java/AsciiDoctor.java
@@ -15,12 +15,12 @@
 import com.google.common.io.ByteStreams;
 import java.io.BufferedReader;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.FileReader;
 import java.io.FilenameFilter;
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -162,7 +162,7 @@
     if (bazel) {
       renderFiles(inputFiles, null);
     } else {
-      try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(zipFile))) {
+      try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(Paths.get(zipFile)))) {
         renderFiles(inputFiles, zip);
 
         File[] cssFiles =
@@ -199,7 +199,7 @@
 
   public static void zipFile(File file, String name, ZipOutputStream zip) throws IOException {
     zip.putNextEntry(new ZipEntry(name));
-    try (FileInputStream input = new FileInputStream(file)) {
+    try (InputStream input = Files.newInputStream(file.toPath())) {
       ByteStreams.copy(input, zip);
     }
     zip.closeEntry();
diff --git a/lib/asciidoctor/java/DocIndexer.java b/lib/asciidoctor/java/DocIndexer.java
index 395f9fe..fbb7f94 100644
--- a/lib/asciidoctor/java/DocIndexer.java
+++ b/lib/asciidoctor/java/DocIndexer.java
@@ -18,13 +18,13 @@
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
 import java.io.FileReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.io.UnsupportedEncodingException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.jar.JarEntry;
@@ -81,7 +81,7 @@
       return;
     }
 
-    try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(outFile))) {
+    try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(Paths.get(outFile)))) {
       byte[] compressedIndex = zip(index());
       JarEntry entry = new JarEntry(String.format("%s/%s", Constants.PACKAGE, Constants.INDEX_ZIP));
       entry.setSize(compressedIndex.length);
@@ -106,7 +106,7 @@
 
         String title;
         try (BufferedReader titleReader =
-            new BufferedReader(new InputStreamReader(new FileInputStream(file), UTF_8))) {
+            new BufferedReader(new InputStreamReader(Files.newInputStream(file.toPath()), UTF_8))) {
           title = titleReader.readLine();
           if (title != null && title.startsWith("[[")) {
             // Generally the first line of the txt is the title. In a few cases the
diff --git a/lib/prolog/java/BuckPrologCompiler.java b/lib/prolog/java/BuckPrologCompiler.java
index d3f41c0..cc3e39e 100644
--- a/lib/prolog/java/BuckPrologCompiler.java
+++ b/lib/prolog/java/BuckPrologCompiler.java
@@ -15,9 +15,9 @@
 import com.googlecode.prolog_cafe.compiler.Compiler;
 import com.googlecode.prolog_cafe.exceptions.CompileException;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
 import java.util.jar.JarEntry;
 import java.util.jar.JarOutputStream;
 
@@ -46,7 +46,7 @@
   private static void jar(File jar, File classes) throws IOException {
     File tmp = File.createTempFile("prolog", ".jar", tmpdir);
     try {
-      try (JarOutputStream out = new JarOutputStream(new FileOutputStream(tmp))) {
+      try (JarOutputStream out = new JarOutputStream(Files.newOutputStream(tmp.toPath()))) {
         add(out, classes, "");
       }
       if (!tmp.renameTo(jar)) {
@@ -70,7 +70,7 @@
       }
 
       JarEntry e = new JarEntry(prefix + name);
-      try (FileInputStream in = new FileInputStream(f)) {
+      try (InputStream in = Files.newInputStream(f.toPath())) {
         e.setTime(f.lastModified());
         out.putNextEntry(e);
         byte[] buf = new byte[16 << 10];
diff --git a/polygerrit-ui/README.md b/polygerrit-ui/README.md
index 0fdbd44..3fd774d 100644
--- a/polygerrit-ui/README.md
+++ b/polygerrit-ui/README.md
@@ -95,8 +95,10 @@
 ./polygerrit-ui/app/run_test.sh
 ```
 
-To allow the tests to run in Safari it is necessary to enable the
-"Allow Remote Automation" option under the "Develop" menu.
+To allow the tests to run in Safari:
+
+* In the Advanced preferences tab, check "Show Develop menu in menu bar".
+* In the Develop menu, enable the "Allow Remote Automation" option.
 
 If you need to pass additional arguments to `wct`:
 
diff --git a/polygerrit-ui/.eslintrc.json b/polygerrit-ui/app/.eslintrc.json
similarity index 100%
rename from polygerrit-ui/.eslintrc.json
rename to polygerrit-ui/app/.eslintrc.json
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 7c12fa2..4e99272 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -34,10 +34,9 @@
     name = "closure_lib",
     srcs = ["gr-app.js"],
     convention = "GOOGLE",
-    language = "ECMASCRIPT6",
-    suppress = [
-        "JSC_BAD_JSDOC_ANNOTATION",
-    ],
+    # TODO(davido): Clean up these issues: http://paste.openstack.org/show/608548
+    # and remove this supression
+    suppress = ["JSC_UNUSED_LOCAL_ASSIGNMENT"],
     deps = [
         "//lib/polymer_externs:polymer_closure",
         "@io_bazel_rules_closure//closure/library",
@@ -52,6 +51,7 @@
     defs = [
         "--polymer_pass",
         "--jscomp_off=duplicate",
+        "--force_inject_library=es6_runtime",
     ],
     language = "ECMASCRIPT5",
     deps = [":closure_lib"],
@@ -157,3 +157,18 @@
         "manual",
     ],
 )
+
+sh_test(
+    name = "lint_test",
+    size = "large",
+    srcs = ["lint_test.sh"],
+    data = [
+        ":pg_code",
+        ".eslintrc.json",
+    ],
+    # Should not run sandboxed.
+    tags = [
+        "local",
+        "manual",
+    ],
+)
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
index 8807917..1c3642d 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
@@ -17,6 +17,17 @@
 (function(window) {
   'use strict';
 
+  // Tags identifying ChangeMessages that move change into WIP state.
+  var WIP_TAGS = [
+    'autogenerated:gerrit:newWipPatchSet',
+    'autogenerated:gerrit:setWorkInProgress',
+  ];
+
+  // Tags identifying ChangeMessages that move change out of WIP state.
+  var READY_TAGS = [
+    'autogenerated:gerrit:setReadyForReview',
+  ];
+
   /** @polymerBehavior Gerrit.PatchSetBehavior */
   var PatchSetBehavior = {
     /**
@@ -37,7 +48,23 @@
       }
     },
 
+    /**
+     * Construct a chronological list of patch sets derived from change details.
+     * Each element of this list is an object with the following properties:
+     *
+     *   * num {number} The number identifying the patch set
+     *   * desc {!string} Optional patch set description
+     *   * wip {boolean} If true, this patch set was never subject to review.
+     *
+     * The wip property is determined by the change's current work_in_progress
+     * property and its log of change messages.
+     *
+     * @param {Object} change The change details
+     * @return {Array<Object>} Sorted list of patch set objects, as described
+     *     above
+     */
     computeAllPatchSets: function(change) {
+      if (!change) { return []; }
       var patchNums = [];
       for (var commit in change.revisions) {
         if (change.revisions.hasOwnProperty(commit)) {
@@ -47,10 +74,45 @@
           });
         }
       }
-      return patchNums.sort(function(a, b) { return a.num - b.num; });
+      patchNums.sort(function(a, b) { return a.num - b.num; });
+      return this._computeWipForPatchSets(change, patchNums);
+    },
+
+    /**
+     * Populate the wip properties of the given list of patch sets.
+     *
+     * @param {Object} change The change details
+     * @param {Array<Object>} patchNums Sorted list of patch set objects, as
+     *     generated by computeAllPatchSets
+     * @return {Array<Object>} The given list of patch set objects, with the
+     *     wip property set on each of them
+     */
+    _computeWipForPatchSets: function(change, patchNums) {
+      if (!change.messages || !change.messages.length) {
+        return patchNums;
+      }
+      var psWip = {};
+      var wip = change.work_in_progress;
+      for (var i = 0; i < change.messages.length; i++) {
+        var msg = change.messages[i];
+        if (WIP_TAGS.indexOf(msg.tag) != -1) {
+          wip = true;
+        } else if (READY_TAGS.indexOf(msg.tag) != -1) {
+          wip = false;
+        }
+        if (psWip[msg._revision_number] !== false) {
+          psWip[msg._revision_number] = wip;
+        }
+      }
+
+      for (var i = 0; i < patchNums.length; i++) {
+        patchNums[i].wip = psWip[patchNums[i].num];
+      }
+      return patchNums;
     },
 
     computeLatestPatchNum: function(allPatchSets) {
+      if (!allPatchSets || !allPatchSets.length) { return undefined; }
       return allPatchSets[allPatchSets.length - 1].num;
     },
 
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
index 892d94b..87e6455 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior_test.html
@@ -79,5 +79,87 @@
             done();
           });
     });
+
+    test('_computeWipForPatchSets', function() {
+      // Compute patch sets for a given timeline on a change. The initial WIP
+      // property of the change can be true or false. The map of tags by
+      // revision is keyed by patch set number. Each value is a list of change
+      // message tags in the order that they occurred in the timeline. These
+      // indicate actions that modify the WIP property of the change and/or
+      // create new patch sets.
+      //
+      // Returns the actual results with an assertWip method that can be used
+      // to compare against an expected value for a particular patch set.
+      function compute(initialWip, tagsByRevision) {
+        var change = {
+          messages: [],
+          work_in_progress: initialWip,
+        };
+        var revs = Object.keys(tagsByRevision).sort(function(a, b) {
+          return a - b;
+        });
+        revs.forEach(function(rev) {
+          tagsByRevision[rev].forEach(function(tag) {
+            change.messages.push({
+              tag: tag,
+              _revision_number: rev,
+            });
+          });
+        });
+        var patchNums = revs.map(function(rev) { return {num: rev}; });
+        patchNums = Gerrit.PatchSetBehavior._computeWipForPatchSets(
+            change, patchNums);
+        var actualWipsByRevision = {};
+        patchNums.forEach(function(patchNum) {
+          actualWipsByRevision[patchNum.num] = patchNum.wip;
+        });
+        var verifier = {
+          assertWip: function(revision, expectedWip) {
+            var patchNum = patchNums.find(function(patchNum) {
+              return patchNum.num == revision;
+            });
+            if (!patchNum) {
+              assert.fail('revision ' + revision + ' not found');
+            }
+            assert.equal(patchNum.wip, expectedWip,
+                'wip state for ' + revision + ' is ' +
+                patchNum.wip + '; expected ' + expectedWip);
+            return verifier;
+          },
+        };
+        return verifier;
+      }
+
+      compute(false, {1: ['upload']}).assertWip(1, false);
+      compute(true, {1: ['upload']}).assertWip(1, true);
+
+      var setWip = 'autogenerated:gerrit:setWorkInProgress';
+      var uploadInWip = 'autogenerated:gerrit:newWipPatchSet';
+      var clearWip = 'autogenerated:gerrit:setReadyForReview';
+
+      compute(false, {
+        1: ['upload', setWip],
+        2: ['upload'],
+        3: ['upload', clearWip],
+        4: ['upload', setWip],
+      }).assertWip(1, false)  // Change was created with PS1 ready for review
+        .assertWip(2, true)   // PS2 was uploaded during WIP
+        .assertWip(3, false)  // PS3 was marked ready for review after upload
+        .assertWip(4, false); // PS4 was uploaded ready for review
+
+      compute(false, {
+        1: [uploadInWip, null, 'addReviewer'],
+        2: ['upload'],
+        3: ['upload', clearWip, setWip],
+        4: ['upload'],
+        5: ['upload', clearWip],
+        6: [uploadInWip],
+      }).assertWip(1, true)  // Change was created in WIP
+        .assertWip(2, true)  // PS2 was uploaded during WIP
+        .assertWip(3, false) // PS3 was marked ready for review
+        .assertWip(4, true)  // PS4 was uploaded during WIP
+        .assertWip(5, false) // PS5 was marked ready for review
+        .assertWip(6, true); // PS6 was uploaded with WIP option
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
index fa8289f..f9c4a80 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior.html
@@ -20,8 +20,8 @@
   /** @polymerBehavior Gerrit.PathListBehavior */
   var PathListBehavior = {
     specialFilePathCompare: function(a, b) {
-      var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
       // The commit message always goes first.
+      var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
       if (a === COMMIT_MESSAGE_PATH) {
         return -1;
       }
@@ -29,6 +29,15 @@
         return 1;
       }
 
+      // The merge list always comes next.
+      var MERGE_LIST_PATH = '/MERGE_LIST';
+      if (a === MERGE_LIST_PATH) {
+        return -1;
+      }
+      if (b === MERGE_LIST_PATH) {
+        return 1;
+      }
+
       var aLastDotIndex = a.lastIndexOf('.');
       var aExt = a.substr(aLastDotIndex + 1);
       var aFile = a.substr(0, aLastDotIndex) || a;
diff --git a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
index adf0bf1..164700f 100644
--- a/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
+++ b/polygerrit-ui/app/behaviors/gr-path-list-behavior/gr-path-list-behavior_test.html
@@ -27,13 +27,14 @@
       var sort = Gerrit.PathListBehavior.specialFilePathCompare;
       var testFiles = [
         '/a.h',
+        '/MERGE_LIST',
         '/a.cpp',
         '/COMMIT_MSG',
         '/asdasd',
         '/mrPeanutbutter.py'
       ];
       assert.deepEqual(testFiles.sort(sort),
-          ['/COMMIT_MSG', '/a.h', '/a.cpp', '/asdasd', '/mrPeanutbutter.py']);
+          ['/COMMIT_MSG', '/MERGE_LIST', '/a.h', '/a.cpp', '/asdasd', '/mrPeanutbutter.py']);
     });
   });
 </script>
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 31721a0..bef92ed 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 @@
     RESTORE: 'restore',
     REVERT: 'revert',
     UNIGNORE: 'unignore',
+    WIP: 'wip',
   };
 
   // TODO(andybons): Add the rest of the revision actions.
@@ -596,6 +597,9 @@
         case ChangeActions.DELETE:
           this._handleDeleteTap();
           break;
+        case ChangeActions.WIP:
+          this._handleWipTap();
+          break;
         default:
           this._fireAction(this._prependSlash(key), this.actions[key], false);
       }
@@ -786,6 +790,9 @@
               page.show(this.changePath(this.changeNum));
             }
             break;
+          case ChangeActions.WIP:
+            page.show(this.changePath(this.changeNum));
+            break;
           default:
             this.dispatchEvent(new CustomEvent('reload-change',
                 {detail: {action: action.__key}, bubbles: false}));
@@ -850,6 +857,10 @@
       this._showActionDialog(this.$.confirmDeleteDialog);
     },
 
+    _handleWipTap: function() {
+      this._fireAction('/wip', this.actions.wip, false);
+    },
+
     /**
      * Merge sources of change actions into a single ordered array of action
      * values.
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
index 778b09c..7408e04 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.html
@@ -255,6 +255,9 @@
         <section class="labelStatus">
           <span class="title">Label Status</span>
           <span class="value">
+            <div hidden$="[[!change.work_in_progress]]">
+              Work in progress
+            </div>
             <div hidden$="[[!_showMissingLabels(change.labels)]]">
               [[_computeMissingLabelsHeader(change.labels)]]
               <ul id="missingLabels">
@@ -265,7 +268,7 @@
                 </template>
               </ul>
             </div>
-            <div hidden$="[[_showMissingLabels(change.labels)]]">
+            <div hidden$="[[_showMissingRequirements(change.labels, change.work_in_progress)]]">
               Ready to submit
             </div>
           </span>
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
index 58938af..9046d1b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata.js
@@ -242,6 +242,10 @@
       return !!this._computeMissingLabels(labels).length;
     },
 
+    _showMissingRequirements: function(labels, workInProgress) {
+      return workInProgress || this._showMissingLabels(labels);
+    },
+
     _computeProjectURL: function(project) {
       return this.getBaseUrl() + '/q/project:' +
         this.encodeURL(project, false);
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
index c531c79..0ac8531 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_test.html
@@ -88,6 +88,17 @@
     });
 
     test('computes submit status', function() {
+      var showMissingLabels = false;
+      sandbox.stub(element, '_showMissingLabels', function() {
+        return showMissingLabels;
+      });
+      assert.isFalse(element._showMissingRequirements(null, false));
+      assert.isTrue(element._showMissingRequirements(null, true));
+      showMissingLabels = true;
+      assert.isTrue(element._showMissingRequirements(null, false));
+    });
+
+    test('show missing labels', function() {
       var labels = {};
       assert.isFalse(element._showMissingLabels(labels));
       labels = {test: {}};
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 2c89e5c..2d57392 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -19,6 +19,7 @@
 <link rel="import" href="../../../behaviors/keyboard-shortcut-behavior/keyboard-shortcut-behavior.html">
 <link rel="import" href="../../../behaviors/rest-client-behavior/rest-client-behavior.html">
 <link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../diff/gr-diff-preferences/gr-diff-preferences.html">
 <link rel="import" href="../../shared/gr-account-link/gr-account-link.html">
 <link rel="import" href="../../shared/gr-select/gr-select.html">
 <link rel="import" href="../../shared/gr-button/gr-button.html">
@@ -68,11 +69,19 @@
         transition: box-shadow 250ms linear;
         width: 100%;
       }
+      .header.wip {
+        background-color: #fcfad6;
+        border-bottom: 1px solid #ddd;
+        margin-bottom: .5em;
+      }
       .header-title {
         flex: 1;
         font-size: 1.2em;
         font-weight: bold;
       }
+      .prefsButton {
+        float: right;
+      }
       gr-change-star {
         margin-right: .25em;
         vertical-align: -.425em;
@@ -188,6 +197,9 @@
         height: 0;
         margin-bottom: 1em;
       }
+      #diffPrefsContainer {
+        margin: auto 0 auto auto;
+      }
       .patchInfo-header-wrapper {
         width: 100%;
       }
@@ -277,7 +289,7 @@
     </style>
     <div class="container loading" hidden$="[[!_loading]]">Loading...</div>
     <div class="container" hidden$="{{_loading}}">
-      <div class="header">
+      <div class$="[[_computeHeaderClass(_change)]]">
         <span class="header-title">
           <gr-change-star
               id="changeStar"
@@ -301,6 +313,7 @@
          --></template><!--
          -->)<!--
        --></template><!--
+       --><span hidden$="[[!_change.work_in_progress]]"> (Work in progress)</span><!--
        -->: [[_change.subject]]
         </span>
       </div>
@@ -425,6 +438,7 @@
                   [[patchNum.num]]
                   /
                   [[computeLatestPatchNum(_allPatchSets)]]
+                  [[_computePatchSetCommentsString(_comments, patchNum.num)]]
                   [[_computePatchSetDescription(_change, patchNum.num)]]
                 </option>
               </template>
@@ -454,9 +468,17 @@
                   read-only="[[_descriptionReadOnly]]"
                   on-changed="_handleDescriptionChanged"></gr-editable-label>
             </span>
+            <span id="diffPrefsContainer"
+                hidden$="[[_computePrefsButtonHidden(_diffPrefs, _loggedIn)]]"
+                hidden>
+              <gr-button link
+                  class="prefsButton desktop"
+                  on-tap="_handlePrefsTap">Diff Preferences</gr-button>
+            </span>
           </div>
         </div>
         <gr-file-list id="fileList"
+            diff-prefs="{{_diffPrefs}}"
             change="[[_change]]"
             change-num="[[_changeNum]]"
             patch-range="{{_patchRange}}"
@@ -500,10 +522,12 @@
           diff-drafts="[[_diffDrafts]]"
           server-config="[[serverConfig]]"
           project-config="[[_projectConfig]]"
+          can-be-started="[[_canStartReview]]"
           on-send="_handleReplySent"
           on-cancel="_handleReplyCancel"
           on-autogrow="_handleReplyAutogrow"
-          hidden$="[[!_loggedIn]]">Reply</gr-reply-dialog>
+          hidden$="[[!_loggedIn]]">
+      </gr-reply-dialog>
     </gr-overlay>
     <gr-js-api-interface id="jsAPI"></gr-js-api-interface>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 1a7765c..ea835e7 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -73,6 +73,7 @@
         type: Object,
         value: function() { return document.body; },
       },
+      _diffPrefs: Object,
       _numFilesShown: {
         type: Number,
         observer: '_numFilesShownChanged',
@@ -81,6 +82,10 @@
         type: Object,
         value: {},
       },
+      _canStartReview: {
+        type: Boolean,
+        computed: '_computeCanStartReview(_loggedIn, _change, _account)',
+      },
       _comments: Object,
       _change: {
         type: Object,
@@ -135,7 +140,7 @@
       _replyButtonLabel: {
         type: String,
         value: 'Reply',
-        computed: '_computeReplyButtonLabel(_diffDrafts.*)',
+        computed: '_computeReplyButtonLabel(_diffDrafts.*, _canStartReview)',
       },
       _selectedPatchSet: String,
       _initialLoadComplete: {
@@ -186,6 +191,7 @@
       'u': '_handleUKey',
       'x': '_handleXKey',
       'z': '_handleZKey',
+      ',': '_handleCommaKey',
     },
 
     attached: function() {
@@ -221,6 +227,10 @@
       }
     },
 
+    _computePrefsButtonHidden: function(prefs, loggedIn) {
+      return !loggedIn || !prefs;
+    },
+
     _handleEditCommitMessage: function(e) {
       this._editingCommitMessage = true;
       this.$.commitMessageEditor.focusTextarea();
@@ -269,6 +279,11 @@
       return false;
     },
 
+    _handlePrefsTap: function(e) {
+      e.preventDefault();
+      this.$.fileList.openDiffPrefs();
+    },
+
     _handleCommentSave: function(e) {
       if (!e.target.comment.__draft) { return; }
 
@@ -562,7 +577,7 @@
     },
 
     _changeChanged: function(change) {
-      if (!change) { return; }
+      if (!change || !this._patchRange || !this._allPatchSets) { return; }
       this.set('_patchRange.basePatchNum',
           this._patchRange.basePatchNum || 'PARENT');
       this.set('_patchRange.patchNum',
@@ -718,7 +733,11 @@
       return result;
     },
 
-    _computeReplyButtonLabel: function(changeRecord) {
+    _computeReplyButtonLabel: function(changeRecord, canStartReview) {
+      if (canStartReview) {
+        return 'Start review';
+      }
+
       var drafts = (changeRecord && changeRecord.base) || {};
       var draftCount = Object.keys(drafts).reduce(function(count, file) {
         return count + drafts[file].length;
@@ -792,6 +811,14 @@
       this.$.messageList.handleExpandCollapse(false);
     },
 
+    _handleCommaKey: function(e) {
+      if (this.shouldSuppressKeyboardShortcut(e) ||
+          this.modifierPressed(e)) { return; }
+
+      e.preventDefault();
+      this.$.fileList.openDiffPrefs();
+    },
+
     _determinePageBack: function() {
       // Default backPage to '/' if user came to change view page
       // via an email link, etc.
@@ -1030,6 +1057,27 @@
           rev.description.substring(0, PATCH_DESC_MAX_LENGTH) : '';
     },
 
+    _computePatchSetCommentsString: function(allComments, patchNum) {
+      var numComments = 0;
+      var numUnresolved = 0;
+      for (var file in allComments) {
+        var comments = allComments[file];
+        numComments += this.$.fileList.getCommentsForPath(
+            allComments, patchNum, file).length;
+        numUnresolved += this.$.fileList.computeUnresolvedNum(
+            allComments, {}, patchNum, file);
+      }
+      var commentsStr = '';
+      if (numComments > 0) {
+        commentsStr = '(' + numComments + ' comments';
+        if (numUnresolved > 0) {
+          commentsStr += ', ' + numUnresolved + ' unresolved';
+        }
+        commentsStr += ')';
+      }
+      return commentsStr;
+    },
+
     _computeDescriptionPlaceholder: function(readOnly) {
       return (readOnly ? 'No' : 'Add a') + ' patch set description';
     },
@@ -1063,6 +1111,11 @@
       }
     },
 
+    _computeCanStartReview: function(loggedIn, change, account) {
+      return !!(loggedIn && change.work_in_progress &&
+          change.owner._account_id === account._account_id);
+    },
+
     _computeDescriptionReadOnly: function(loggedIn, change, account) {
       return !(loggedIn && (account._account_id === change.owner._account_id));
     },
@@ -1177,10 +1230,14 @@
       this._updateCheckTimerHandle = this.async(function() {
         this.fetchIsLatestKnown(this._change, this.$.restAPI)
             .then(function(latest) {
-              if (!latest) {
+              if (latest) {
+                this._startUpdateCheckTimer();
+              } else {
                 this._cancelUpdateCheckTimer();
                 this.fire('show-alert', {
                   message: 'A newer patch has been uploaded.',
+                  // Persist this alert.
+                  dismissOnNavigation: true,
                   action: 'Reload',
                   callback: function() {
                     // Load the current change without any patch range.
@@ -1189,7 +1246,6 @@
                   }.bind(this),
                 });
               }
-              this._startUpdateCheckTimer();
             }.bind(this));
       }, this.serverConfig.change.update_delay * 1000);
     },
@@ -1206,5 +1262,9 @@
         this._startUpdateCheckTimer();
       }
     },
+
+    _computeHeaderClass: function(change) {
+      return change.work_in_progress ? 'header wip' : 'header';
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index 59b3d41..5e932f8d 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -98,6 +98,7 @@
 
       test('A toggles overlay when logged in', function(done) {
         sandbox.stub(element, '_getLoggedIn').returns(Promise.resolve(true));
+        element._change = {labels: {}};
         MockInteractions.pressAndReleaseKeyOn(element, 65, null, 'a');
         flush(function() {
           assert.isTrue(element.$.replyOverlay.opened);
@@ -156,6 +157,45 @@
         MockInteractions.pressAndReleaseKeyOn(element, 68, null, 'd');
         assert.isTrue(stub.called);
       });
+
+      test(', should open diff preferences', function() {
+        var stub = sandbox.stub(element.$.fileList.$.diffPreferences, 'open');
+        MockInteractions.pressAndReleaseKeyOn(element, 188, null, ',');
+        assert.isTrue(stub.called);
+      });
+    });
+
+    test('Diff preferences hidden when no prefs or logged out',
+        function() {
+      element._loggedIn = false;
+      flushAsynchronousOperations();
+      assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+      element._loggedIn = true;
+      flushAsynchronousOperations();
+      assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+      element._loggedIn = false;
+      element._diffPrefs = {'font_size': '12'};
+      flushAsynchronousOperations();
+      assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+      element._loggedIn = true;
+      flushAsynchronousOperations();
+      assert.isFalse(element.$.diffPrefsContainer.hidden);
+    });
+
+    test('prefsButton opens gr-diff-preferences', function() {
+      var handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap');
+      var overlayOpenStub = sandbox.stub(element.$.fileList,
+          'openDiffPrefs');
+      var prefsButton = Polymer.dom(element.root).querySelectorAll(
+          '.prefsButton')[0];
+
+      MockInteractions.tap(prefsButton);
+
+      assert.isTrue(handlePrefsTapSpy.called);
+      assert.isTrue(overlayOpenStub.called);
     });
 
     test('_computeDescriptionReadOnly', function() {
@@ -197,6 +237,45 @@
       assert.equal(result, 'R=\u200Btest@google.com\nR=\u200Btest@google.com');
     }),
 
+    test('_computePatchSetCommentsString', function() {
+      // Test string with unresolved comments.
+      comments = {
+        'foo': 'foo comments',
+        'bar': 'bar comments',
+        'xyz': 'xyz comments',
+      };
+      sandbox.stub(element.$.fileList, 'getCommentsForPath', function(c, p, f) {
+        if (f == 'foo') {
+          return ['comment1', 'comment2'];
+        } else if (f == 'bar') {
+          return ['comment1'];
+        } else {
+          return [];
+        }
+      });
+      sandbox.stub(
+          element.$.fileList, 'computeUnresolvedNum', function (c, d, p, f) {
+        if (f == 'foo') {
+          return 0;
+        } else if (f == 'bar') {
+          return 1;
+        } else {
+          return 0;
+        }
+      });
+      assert.equal(element._computePatchSetCommentsString(comments, 1),
+          '(3 comments, 1 unresolved)');
+
+      // Test string with no unresolved comments.
+      delete comments['bar']
+      assert.equal(element._computePatchSetCommentsString(comments, 1),
+          '(2 comments)');
+
+      // Test string with no comments.
+      delete comments['foo']
+      assert.equal(element._computePatchSetCommentsString(comments, 1), '');
+    });
+
     test('_handleDescriptionChanged', function() {
       var putDescStub = sandbox.stub(element.$.restAPI, 'setDescription')
           .returns(Promise.resolve({ok: true}));
@@ -309,21 +388,28 @@
     });
 
     test('reply button has updated count when there are drafts', function() {
-      var replyButton = element.$$('gr-button.reply');
-      assert.ok(replyButton);
-      assert.equal(replyButton.textContent, 'Reply');
+      var getLabel = element._computeReplyButtonLabel;
 
-      element._diffDrafts = null;
-      assert.equal(replyButton.textContent, 'Reply');
+      assert.equal(getLabel(null, false), 'Reply');
+      assert.equal(getLabel(null, true), 'Start review');
 
-      element._diffDrafts = {};
-      assert.equal(replyButton.textContent, 'Reply');
+      var changeRecord = {base: null};
+      assert.equal(getLabel(changeRecord, false), 'Reply');
 
-      element._diffDrafts = {
+      changeRecord.base = {};
+      assert.equal(getLabel(changeRecord, false), 'Reply');
+
+      changeRecord.base = {
         'file1.txt': [{}],
         'file2.txt': [{}, {}],
       };
-      assert.equal(replyButton.textContent, 'Reply (3)');
+      assert.equal(getLabel(changeRecord, false), 'Reply (3)');
+    });
+
+    test('start review button when owner of WIP change', function() {
+      assert.equal(
+          element._computeReplyButtonLabel(null, true),
+          'Start review');
     });
 
     test('comment events properly update diff drafts', function() {
@@ -940,6 +1026,7 @@
     suite('reply dialog tests', function() {
       setup(function() {
         sandbox.stub(element.$.replyDialog, '_draftChanged');
+        element._change = {labels: {}};
       });
 
       test('reply from comment adds quote text', function() {
@@ -1144,6 +1231,27 @@
           element.serverConfig = {change: {update_delay: 12345}};
         });
       });
+
+      test('canStartReview computation', function() {
+        var account1 = {_account_id: 1};
+        var account2 = {_account_id: 2};
+        var change = {
+          owner: {_account_id: 1},
+        };
+        assert.isFalse(element._computeCanStartReview(true, change, account1));
+        change.work_in_progress = false;
+        assert.isFalse(element._computeCanStartReview(true, change, account1));
+        change.work_in_progress = true;
+        assert.isTrue(element._computeCanStartReview(true, change, account1));
+        assert.isFalse(element._computeCanStartReview(false, change, account1));
+        assert.isFalse(element._computeCanStartReview(true, change, account2));
+      });
+
+      test('header class computation', function() {
+        assert.equal(element._computeHeaderClass({}), 'header');
+        assert.equal(element._computeHeaderClass({work_in_progress: true}),
+            'header wip');
+      });
     });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
index 0e640a7..8ad2c19 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.html
@@ -53,7 +53,9 @@
     </style>
     <template is="dom-repeat" items="[[_computeFilesFromComments(comments)]]" as="file">
       <div class="file">
-        <a href$="[[_computeFileDiffURL(file, changeNum, patchNum)]]">[[file]]</a>:
+        <a href$="[[_computeFileDiffURL(file, changeNum, patchNum)]]">
+          [[_computeFileDisplayName(file)]]
+        </a>:
       </div>
       <template is="dom-repeat"
                 items="[[_computeCommentsForFile(comments, file)]]" as="comment">
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
index f1fb0fd..98a2508 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list.js
@@ -14,6 +14,9 @@
 (function() {
   'use strict';
 
+  var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
+  var MERGE_LIST_PATH = '/MERGE_LIST';
+
   Polymer({
     is: 'gr-comment-list',
 
@@ -39,6 +42,15 @@
         '/' + patchNum + '/' + file;
     },
 
+    _computeFileDisplayName: function(path) {
+      if (path === COMMIT_MESSAGE_PATH) {
+        return 'Commit message';
+      } else if (path === MERGE_LIST_PATH) {
+        return 'Merge list';
+      }
+      return path;
+    },
+
     _isOnParent: function(comment) {
       return comment.side === 'PARENT';
     },
diff --git a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
index 34f2951..e27bad0 100644
--- a/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-comment-list/gr-comment-list_test.html
@@ -65,6 +65,15 @@
       assert.equal(actual, expected);
     });
 
+    test('_computeFileDisplayName', function() {
+      assert.equal(element._computeFileDisplayName('/COMMIT_MSG'),
+          'Commit message');
+      assert.equal(element._computeFileDisplayName('/MERGE_LIST'),
+          'Merge list');
+      assert.equal(element._computeFileDisplayName('/foo/bar/baz'),
+          '/foo/bar/baz');
+    });
+
     test('_computeDiffLineURL', function() {
       var comment = {line: 123, side: 'REVISION', patch_set: 10};
       var expected = '/c/<change>/<patch>/<file>#123';
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 2bcbac3..af412db 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -72,6 +72,7 @@
         background-color: #ebf5fb;
       }
       .path {
+        cursor: pointer;
         flex: 1;
         padding-left: .35em;
         text-decoration: none;
@@ -222,10 +223,11 @@
             <option value="PARENT">Base</option>
             <template
                 is="dom-repeat"
-                items="[[_computePatchSets(revisions.*, patchRange.*)]]"
+                items="[[computeAllPatchSets(change)]]"
                 as="patchNum">
-              <option value$="[[patchNum.num]]"
-                  disabled$="[[_computePatchSetDisabled(patchNum.num, patchRange.patchNum)]]">
+              <option
+                  disabled$="[[_computePatchSetDisabled(patchNum.num, patchRange.patchNum)]]"
+                  value$="[[patchNum.num]]">
                 [[patchNum.num]]
                 [[patchNum.desc]]
               </option>
@@ -241,10 +243,9 @@
           as="file"
           initial-count="[[_fileListIncrement]]"
           target-framerate="1">
-        <div class="file-row row">
+        <div class="file-row row" data-path$="[[file.__path]]">
           <div class="reviewed" hidden$="[[!_loggedIn]]" hidden>
             <input type="checkbox" checked="[[file.isReviewed]]"
-                data-path$="[[file.__path]]"
                 class="reviewed" aria-label="Reviewed checkbox">
           </div>
           <div class$="[[_computeClass('status', file.__path)]]"
@@ -252,22 +253,24 @@
               aria-label$="[[_computeFileStatusLabel(file.status)]]">
             [[_computeFileStatus(file.status)]]
           </div>
-          <a class$="[[_computePathClass(file.__path, _expandedFilePaths.*)]]"
-              href$="[[_computeDiffURL(changeNum, patchRange, file.__path)]]"
-              data-path$="[[file.__path]]">
-            <div title$="[[_computeFileDisplayName(file.__path)]]"
-                class="fullFileName">
-              [[_computeFileDisplayName(file.__path)]]
-            </div>
-            <div title$="[[_computeFileDisplayName(file.__path)]]"
-                class="truncatedFileName">
-              [[_computeTruncatedFileDisplayName(file.__path)]]
-            </div>
+          <span
+              data-url="[[_computeDiffURL(changeNum, patchRange, file.__path)]]"
+              class$="[[_computePathClass(file.__path, _expandedFilePaths.*)]]">
+            <a href$="[[_computeDiffURL(changeNum, patchRange, file.__path)]]">
+              <span title$="[[_computeFileDisplayName(file.__path)]]"
+                  class="fullFileName">
+                [[_computeFileDisplayName(file.__path)]]
+              </span>
+              <span title$="[[_computeFileDisplayName(file.__path)]]"
+                  class="truncatedFileName">
+                [[_computeTruncatedFileDisplayName(file.__path)]]
+              </span>
+            </a>
             <div class="oldPath" hidden$="[[!file.old_path]]" hidden
                 title$="[[file.old_path]]">
               [[file.old_path]]
             </div>
-          </a>
+          </span>
           <div class="comments desktop">
             <span class="drafts">
               [[_computeDraftsString(drafts, patchRange.patchNum, file.__path)]]
@@ -323,9 +326,10 @@
               change-num="[[changeNum]]"
               patch-range="[[patchRange]]"
               path="[[file.__path]]"
-              prefs="[[_diffPrefs]]"
+              prefs="[[diffPrefs]]"
               project-config="[[projectConfig]]"
               on-line-selected="_onLineSelected"
+              no-render-on-prefs-change
               view-mode="[[_getDiffViewMode(diffViewMode, _userPrefs)]]"></gr-diff>
         </template>
       </template>
@@ -379,6 +383,10 @@
         [[_computeShowAllText(_files)]]
       </gr-button><!--
  --></gr-tooltip-content>
+    <gr-diff-preferences
+        id="diffPreferences"
+        prefs="{{diffPrefs}}"
+        local-prefs="{{_localPrefs}}"></gr-diff-preferences>
     <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
     <gr-storage id="storage"></gr-storage>
     <gr-diff-cursor id="diffCursor"></gr-diff-cursor>
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 2a166ab..30ccb76 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
@@ -18,6 +18,7 @@
   var PATCH_DESC_MAX_LENGTH = 500;
   var WARN_SHOW_ALL_THRESHOLD = 1000;
   var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
+  var MERGE_LIST_PATH = '/MERGE_LIST';
 
   var FileStatus = {
     A: 'Added',
@@ -53,6 +54,7 @@
       diffViewMode: {
         type: String,
         notify: true,
+        observer: '_updateDiffPreferences',
       },
       _files: {
         type: Array,
@@ -68,7 +70,11 @@
         value: function() { return []; },
       },
       _diffAgainst: String,
-      _diffPrefs: Object,
+      diffPrefs: {
+        type: Object,
+        notify: true,
+        observer: '_updateDiffPreferences',
+      },
       _userPrefs: Object,
       _localPrefs: Object,
       _showInlineDiffs: Boolean,
@@ -157,7 +163,7 @@
 
       this._localPrefs = this.$.storage.getPreferences();
       promises.push(this._getDiffPreferences().then(function(prefs) {
-        this._diffPrefs = prefs;
+        this.diffPrefs = prefs;
       }.bind(this)));
 
       promises.push(this._getPreferences().then(function(prefs) {
@@ -172,6 +178,10 @@
       return Polymer.dom(this.root).querySelectorAll('gr-diff');
     },
 
+    openDiffPrefs: function() {
+      this.$.diffPreferences.open();
+    },
+
     _calculatePatchChange: function(files) {
       var filesNoCommitMsg = files.filter(function(files) {
         return files.__path !== '/COMMIT_MSG';
@@ -205,20 +215,6 @@
       return this.$.restAPI.getPreferences();
     },
 
-    _computePatchSets: function(revisionRecord) {
-      var revisions = revisionRecord.base;
-      var patchNums = [];
-      for (var commit in revisions) {
-        if (revisions.hasOwnProperty(commit)) {
-          patchNums.push({
-            num: revisions[commit]._number,
-            desc: revisions[commit].description,
-          });
-        }
-      }
-      return patchNums.sort(function(a, b) { return a.num - b.num; });
-    },
-
     _computePatchSetDisabled: function(patchNum, currentPatchNum) {
       return parseInt(patchNum, 10) >= parseInt(currentPatchNum, 10);
     },
@@ -245,6 +241,19 @@
           this._patchRangeStr(patchRange), true));
     },
 
+    _updateDiffPreferences: function() {
+      if (!this.diffs.length) { return; }
+      // Re-render all expanded diffs sequentially.
+      var timerName = 'Update ' + this._expandedFilePaths.length +
+          ' diffs with new prefs';
+      this._renderInOrder(this._expandedFilePaths, this.diffs,
+          this._expandedFilePaths.length)
+          .then(function() {
+            this.$.reporting.timeEnd(timerName);
+            this.$.diffCursor.handleDiffUpdate();
+          }.bind(this));
+    },
+
     _forEachDiff: function(fn) {
       var diffs = this.diffs;
       for (var i = 0; i < diffs.length; i++) {
@@ -293,7 +302,7 @@
       return commentCount ? commentCount + 'c' : '';
     },
 
-    _getCommentsForPath: function(comments, patchNum, path) {
+    getCommentsForPath: function(comments, patchNum, path) {
       return (comments[path] || []).filter(function(c) {
         return parseInt(c.patch_set, 10) === parseInt(patchNum, 10);
       });
@@ -302,7 +311,7 @@
     _computeCountString: function(comments, patchNum, path, opt_noun) {
       if (!comments) { return ''; }
 
-      var patchComments = this._getCommentsForPath(comments, patchNum, path);
+      var patchComments = this.getCommentsForPath(comments, patchNum, path);
       var num = patchComments.length;
       if (num === 0) { return ''; }
       if (!opt_noun) { return num; }
@@ -321,8 +330,14 @@
      * @return {string}
      */
     _computeUnresolvedString: function(comments, drafts, patchNum, path) {
-      comments = this._getCommentsForPath(comments, patchNum, path);
-      drafts = this._getCommentsForPath(drafts, patchNum, path);
+      var unresolvedNum = this.computeUnresolvedNum(
+          comments, drafts, patchNum, path);
+      return unresolvedNum === 0 ? '' : '(' + unresolvedNum + ' unresolved)';
+    },
+
+    computeUnresolvedNum: function(comments, drafts, patchNum, path) {
+      comments = this.getCommentsForPath(comments, patchNum, path);
+      drafts = this.getCommentsForPath(drafts, patchNum, path);
       comments = comments.concat(drafts);
 
       // Create an object where every comment ID is the key of an unresolved
@@ -345,18 +360,13 @@
         return idMap[key];
       });
 
-      return unresolvedLeaves.length === 0 ?
-          '' : '(' + unresolvedLeaves.length + ' unresolved)';
+      return unresolvedLeaves.length;
     },
 
     _computeReviewed: function(file, _reviewed) {
       return _reviewed.indexOf(file.__path) !== -1;
     },
 
-    _handleReviewedChange: function(e) {
-      this._reviewFile(Polymer.dom(e).rootTarget.getAttribute('data-path'));
-    },
-
     _reviewFile: function(path) {
       var index = this._reviewed.indexOf(path);
       var reviewed = index !== -1;
@@ -398,24 +408,38 @@
      * have to get registered for potentially very long lists.
      */
     _handleFileListTap: function(e) {
+      // Traverse upwards to find the row element if the target is not the row.
+      var row = e.target;
+      while (!row.classList.contains('row') && row.parentElement) {
+        row = row.parentElement;
+      }
+      var path = row.dataset.path;
+
       // Handle checkbox mark as reviewed.
       if (e.target.classList.contains('reviewed')) {
-        return this._handleReviewedChange(e);
+        return this._reviewFile(path);
       }
 
-      // Check to see if the file should be expanded.
-      var path = e.target.dataset.path || e.target.parentElement.dataset.path;
-
       // If the user prefers to expand inline diffs rather than opening the diff
       // view, intercept the click event.
       if (!path || e.detail.sourceEvent.metaKey ||
           e.detail.sourceEvent.ctrlKey) {
-          return;
+        return;
       }
+
       if (e.target.dataset.expand ||
           this._userPrefs && this._userPrefs.expand_inline_diffs) {
         e.preventDefault();
         this._togglePathExpanded(path);
+        return;
+      }
+
+      // If we clicked the row but not the link, then simulate a click on the
+      // anchor.
+      if (e.target.classList.contains('path') ||
+          e.target.classList.contains('oldPath')) {
+        var a = row.querySelector('a');
+        if (a) { a.click(); }
       }
     },
 
@@ -617,12 +641,16 @@
     },
 
     _computeFileDisplayName: function(path) {
-      return path === COMMIT_MESSAGE_PATH ? 'Commit message' : path;
+      if (path === COMMIT_MESSAGE_PATH) {
+        return 'Commit message';
+      } else if (path === MERGE_LIST_PATH) {
+        return 'Merge list';
+      }
+      return path;
     },
 
     _computeTruncatedFileDisplayName: function(path) {
-      return path === COMMIT_MESSAGE_PATH ?
-          'Commit message' : util.truncatePath(path);
+      return util.truncatePath(this._computeFileDisplayName(path));
     },
 
     _formatBytes: function(bytes) {
@@ -652,7 +680,7 @@
 
     _computeClass: function(baseClass, path) {
       var classes = [baseClass];
-      if (path === COMMIT_MESSAGE_PATH) {
+      if (path === COMMIT_MESSAGE_PATH || path === MERGE_LIST_PATH) {
         classes.push('invisible');
       }
       return classes.join(' ');
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index 4abd68a..d07f364 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -522,9 +522,13 @@
       assert.equal(
           element._computeUnresolvedString(comments, [], 2, 'myfile.txt'), '');
       assert.equal(
+          element.computeUnresolvedNum(comments, [], 2, 'myfile.txt'), 0);
+      assert.equal(
           element._computeUnresolvedString(comments, [], 2, 'unresolved.file'),
           '(1 unresolved)');
       assert.equal(
+          element.computeUnresolvedNum(comments, [], 2, 'unresolved.file'), 1);
+      assert.equal(
           element._computeUnresolvedString(comments, drafts, 2,
           'unresolved.file'), '');
     });
@@ -590,8 +594,8 @@
         {num: 3, desc: 'test'},
         {num: 4, desc: 'test'},
       ];
-      var patchNums = element._computePatchSets({
-        base: {
+      var patchNums = element.computeAllPatchSets({
+        revisions: {
           rev3: {_number: 3, description: 'test'},
           rev1: {_number: 1, description: 'test'},
           rev4: {_number: 4, description: 'test'},
@@ -620,10 +624,12 @@
         basePatchNum: 'PARENT',
         patchNum: '3',
       };
-      element.revisions = {
-        rev1: {_number: 1},
-        rev2: {_number: 2},
-        rev3: {_number: 3},
+      element.change = {
+        revisions: {
+          rev1: {_number: 1},
+          rev2: {_number: 2},
+          rev3: {_number: 3},
+        },
       };
       flush(function() {
         var selectEl = element.$.patchChange;
@@ -693,6 +699,7 @@
         basePatchNum: 'PARENT',
         patchNum: '2',
       };
+      sandbox.spy(element, '_updateDiffPreferences');
       element.$.fileCursor.setCursorAtIndex(0);
       flushAsynchronousOperations();
 
@@ -708,6 +715,7 @@
       assert.equal(diffDisplay.viewMode, 'SIDE_BY_SIDE');
       element.set('diffViewMode', 'UNIFIED_DIFF');
       assert.equal(diffDisplay.viewMode, 'UNIFIED_DIFF');
+      assert.isTrue(element._updateDiffPreferences.called);
     });
 
     test('diff mode selector initializes from preferences', function() {
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
index d08961c..d9cdcc0 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.html
@@ -204,7 +204,7 @@
             id="textarea"
             class="message"
             autocomplete="on"
-            placeholder="Say something nice..."
+            placeholder=[[_messagePlaceholder]]
             disabled="{{disabled}}"
             rows="4"
             max-rows="15"
@@ -248,7 +248,14 @@
             primary
             disabled="[[!_isState(knownLatestState, 'latest')]]"
             class="action send"
-            on-tap="_sendTapHandler">Send</gr-button>
+            on-tap="_sendTapHandler">[[_sendButtonLabel]]</gr-button>
+        </gr-button>
+        <template is="dom-if" if="[[canBeStarted]]">
+          <gr-button
+              disabled="[[!_isState(knownLatestState, 'latest')]]"
+              class="action save"
+              on-tap="_saveTapHandler">Save</gr-button>
+        </template>
         <span
             id="checkingStatusLabel"
             hidden$="[[!_isState(knownLatestState, 'checking')]]">
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
index 0bb548b..79a1f34 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.js
@@ -65,6 +65,10 @@
     properties: {
       change: Object,
       patchNum: String,
+      canBeStarted: {
+        type: Boolean,
+        value: false,
+      },
       disabled: {
         type: Boolean,
         value: false,
@@ -90,6 +94,10 @@
       serverConfig: Object,
       projectConfig: Object,
       knownLatestState: String,
+      underReview: {
+        type: Boolean,
+        value: true,
+      },
 
       _account: Object,
       _ccs: Array,
@@ -97,6 +105,10 @@
         type: Object,
         observer: '_reviewerPendingConfirmationUpdated',
       },
+      _messagePlaceholder: {
+        type: String,
+        computed: '_computeMessagePlaceholder(canBeStarted)',
+      },
       _owner: Object,
       _pendingConfirmationDetails: Object,
       _includeComments: {
@@ -120,6 +132,10 @@
           REVIEWER: [],
         },
       },
+      _sendButtonLabel: {
+        type: String,
+        computed: '_computeSendButtonLabel(canBeStarted)',
+      },
     },
 
     FocusTarget: FocusTarget,
@@ -177,7 +193,8 @@
     },
 
     setLabelValue: function(label, value) {
-      var selectorEl = this.$$('iron-selector[data-label="' + label + '"]');
+      var selectorEl =
+          this.$.labelScores.$$('iron-selector[data-label="' + label + '"]');
       // The selector may not be present if it’s not at the latest patch set.
       if (!selectorEl) { return; }
       var item = selectorEl.$$('gr-button[data-value="' + value + '"]');
@@ -416,6 +433,12 @@
       if (total > 1) { return total + ' Drafts'; }
     },
 
+    _computeMessagePlaceholder: function(canBeStarted) {
+      return canBeStarted ?
+        'Add a note for your reviewers...' :
+        'Say something nice...';
+    },
+
     _changeUpdated: function(changeRecord, owner, serverConfig) {
       this._rebuildReviewerArrays(changeRecord.base, owner, serverConfig);
     },
@@ -498,18 +521,39 @@
           this.serverConfig);
     },
 
-    _sendTapHandler: function(e) {
+    _saveTapHandler: function(e) {
       e.preventDefault();
       this.send(this._includeComments).then(function(keepReviewers) {
         this._purgeReviewersPendingRemove(false, keepReviewers);
       }.bind(this));
     },
 
+    _sendTapHandler: function(e) {
+      e.preventDefault();
+      if (this.canBeStarted) {
+        this._startReview()
+          .then(function() {
+            return this.send(this._includeComments);
+          }.bind(this))
+          .then(this._purgeReviewersPendingRemove.bind(this));
+        return;
+      }
+      this.send(this._includeComments)
+        .then(this._purgeReviewersPendingRemove.bind(this));
+    },
+
     _saveReview: function(review, opt_errFn) {
       return this.$.restAPI.saveChangeReview(this.change._number, this.patchNum,
           review, opt_errFn);
     },
 
+    _startReview: function() {
+      if (!this.canBeStarted) {
+        return Promise.resolve();
+      }
+      return this.$.restAPI.startReview(this.change._number);
+    },
+
     _reviewerPendingConfirmationUpdated: function(reviewer) {
       if (reviewer === null) {
         this.$.reviewerConfirmationOverlay.close();
@@ -582,5 +626,9 @@
       // Load the current change without any patch range.
       location.href = this.getBaseUrl() + '/c/' + this.change._number;
     },
+
+    _computeSendButtonLabel: function(canBeStarted) {
+      return canBeStarted ? "Start review" : "Send";
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
index 253f01c..55a2c9c 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog_test.html
@@ -214,6 +214,17 @@
       });
     });
 
+    test('setlabelValue', function() {
+      element._account = {_account_id: 1};
+      flushAsynchronousOperations();
+      var label = 'Verified';
+      var value = '+1';
+      element.setLabelValue(label, value);
+      flushAsynchronousOperations();
+      var labels = element.$.labelScores.getLabelValues();
+      assert.deepEqual(labels, {'Verified': 1});
+    });
+
     function getActiveElement() {
       return Polymer.IronOverlayManager.deepActiveElement;
     }
@@ -665,5 +676,23 @@
 
       assert.isTrue(cancelHandler.called);
     });
+
+    test('_computeMessagePlaceholder', function() {
+      assert.equal(
+          element._computeMessagePlaceholder(false),
+          'Say something nice...');
+      assert.equal(
+          element._computeMessagePlaceholder(true),
+          'Add a note for your reviewers...');
+    });
+
+    test('_computeSendButtonLabel', function() {
+      assert.equal(
+          element._computeSendButtonLabel(false),
+          'Send');
+      assert.equal(
+          element._computeSendButtonLabel(true),
+          'Start review');
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
index 27dcd4a..1ccb30b 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager.js
@@ -14,13 +14,13 @@
 (function() {
   'use strict';
 
-  var HIDE_ALERT_TIMEOUT_MS = 5000;
-  var CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
-  var STALE_CREDENTIAL_THRESHOLD_MS = 10 * 60 * 1000;
-  var SIGN_IN_WIDTH_PX = 690;
-  var SIGN_IN_HEIGHT_PX = 500;
-  var TOO_MANY_FILES = 'too many files to find conflicts';
-  var AUTHENTICATION_REQUIRED = 'Authentication required\n';
+  const HIDE_ALERT_TIMEOUT_MS = 5000;
+  const CHECK_SIGN_IN_INTERVAL_MS = 60 * 1000;
+  const STALE_CREDENTIAL_THRESHOLD_MS = 10 * 60 * 1000;
+  const SIGN_IN_WIDTH_PX = 690;
+  const SIGN_IN_HEIGHT_PX = 500;
+  const TOO_MANY_FILES = 'too many files to find conflicts';
+  const AUTHENTICATION_REQUIRED = 'Authentication required\n';
 
   Polymer({
     is: 'gr-error-manager',
@@ -48,11 +48,11 @@
        */
       _lastCredentialCheck: {
         type: Number,
-        value: function() { return Date.now(); },
-      }
+        value() { return Date.now(); },
+      },
     },
 
-    attached: function() {
+    attached() {
       this.listen(document, 'server-error', '_handleServerError');
       this.listen(document, 'network-error', '_handleNetworkError');
       this.listen(document, 'show-alert', '_handleShowAlert');
@@ -60,7 +60,7 @@
       this.listen(document, 'show-auth-required', '_handleAuthRequired');
     },
 
-    detached: function() {
+    detached() {
       this._clearHideAlertHandle();
       this.unlisten(document, 'server-error', '_handleServerError');
       this.unlisten(document, 'network-error', '_handleNetworkError');
@@ -68,21 +68,21 @@
       this.unlisten(document, 'visibilitychange', '_handleVisibilityChange');
     },
 
-    _shouldSuppressError: function(msg) {
-      return msg.indexOf(TOO_MANY_FILES) > -1;
+    _shouldSuppressError(msg) {
+      return msg.includes(TOO_MANY_FILES);
     },
 
-    _handleAuthRequired: function() {
+    _handleAuthRequired() {
       this._showAuthErrorAlert(
           'Log in is required to perform that action.', 'Log in.');
     },
 
-    _handleServerError: function(e) {
+    _handleServerError(e) {
       Promise.all([
-        e.detail.response.text(), this._getLoggedIn()
-      ]).then(function(values) {
-        var text = values[0];
-        var loggedIn = values[1];
+        e.detail.response.text(), this._getLoggedIn(),
+      ]).then(values => {
+        const text = values[0];
+        const loggedIn = values[1];
         if (e.detail.response.status === 403 &&
             loggedIn &&
             text === AUTHENTICATION_REQUIRED) {
@@ -92,48 +92,58 @@
         } else if (!this._shouldSuppressError(text)) {
           this._showAlert('Server error: ' + text);
         }
-      }.bind(this));
+      });
     },
 
-    _handleShowAlert: function(e) {
-      this._showAlert(e.detail.message, e.detail.action, e.detail.callback);
+    _handleShowAlert(e) {
+      this._showAlert(e.detail.message, e.detail.action, e.detail.callback,
+          e.detail.dismissOnNavigation);
     },
 
-    _handleNetworkError: function(e) {
+    _handleNetworkError(e) {
       this._showAlert('Server unavailable');
       console.error(e.detail.error.message);
     },
 
-    _getLoggedIn: function() {
+    _getLoggedIn() {
       return this.$.restAPI.getLoggedIn();
     },
 
-    _showAlert: function(text, opt_actionText, opt_actionCallback) {
+    _showAlert(text, opt_actionText, opt_actionCallback,
+        dismissOnNavigation) {
       if (this._alertElement) { return; }
 
       this._clearHideAlertHandle();
-      this._hideAlertHandle =
-        this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS);
-      var el = this._createToastAlert();
+      if (dismissOnNavigation) {
+        // Persist alert until navigation.
+        this.listen(document, 'location-change', '_hideAlert');
+      } else {
+        this._hideAlertHandle =
+          this.async(this._hideAlert, HIDE_ALERT_TIMEOUT_MS);
+      }
+      const el = this._createToastAlert();
       el.show(text, opt_actionText, opt_actionCallback);
       this._alertElement = el;
     },
 
-    _hideAlert: function() {
+    _hideAlert() {
       if (!this._alertElement) { return; }
 
       this._alertElement.hide();
       this._alertElement = null;
+
+      // Remove listener for page navigation, if it exists.
+      this.unlisten(document, 'location-change', '_hideAlert');
     },
 
-    _clearHideAlertHandle: function() {
+    _clearHideAlertHandle() {
       if (this._hideAlertHandle != null) {
         this.cancelAsync(this._hideAlertHandle);
         this._hideAlertHandle = null;
       }
     },
 
-    _showAuthErrorAlert: function(errorText, actionText) {
+    _showAuthErrorAlert(errorText, actionText) {
       // TODO(viktard): close alert if it's not for auth error.
       if (this._alertElement) { return; }
 
@@ -148,13 +158,13 @@
       }
     },
 
-    _createToastAlert: function() {
-      var el = document.createElement('gr-alert');
+    _createToastAlert() {
+      const el = document.createElement('gr-alert');
       el.toast = true;
       return el;
     },
 
-    _handleVisibilityChange: function() {
+    _handleVisibilityChange() {
       // Ignore when the page is transitioning to hidden (or hidden is
       // undefined).
       if (document.hidden !== false) { return; }
@@ -162,7 +172,7 @@
       // If not currently refreshing credentials and the credentials are old,
       // request them to confirm their validity or (display an auth toast if it
       // fails).
-      var timeSinceLastCheck = Date.now() - this._lastCredentialCheck;
+      const timeSinceLastCheck = Date.now() - this._lastCredentialCheck;
       if (!this._refreshingCredentials &&
           this.knownAccountId !== undefined &&
           timeSinceLastCheck > STALE_CREDENTIAL_THRESHOLD_MS) {
@@ -171,18 +181,17 @@
       }
     },
 
-    _requestCheckLoggedIn: function() {
+    _requestCheckLoggedIn() {
       this.debounce(
-        'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS);
+          'checkLoggedIn', this._checkSignedIn, CHECK_SIGN_IN_INTERVAL_MS);
     },
 
-    _checkSignedIn: function() {
-      this.$.restAPI.checkCredentials().then(function(account) {
-        var isLoggedIn = !!account;
+    _checkSignedIn() {
+      this.$.restAPI.checkCredentials().then(account => {
+        const isLoggedIn = !!account;
         this._lastCredentialCheck = Date.now();
         if (this._refreshingCredentials) {
           if (isLoggedIn) {
-
             // If the credentials were refreshed but the account is different
             // then reload the page completely.
             if (account._account_id !== this.knownAccountId) {
@@ -195,17 +204,19 @@
             this._requestCheckLoggedIn();
           }
         }
-      }.bind(this));
+      });
     },
 
-    _reloadPage: function() {
+    _reloadPage() {
       window.location.reload();
     },
 
-    _createLoginPopup: function() {
-      var left = window.screenLeft + (window.outerWidth - SIGN_IN_WIDTH_PX) / 2;
-      var top = window.screenTop + (window.outerHeight - SIGN_IN_HEIGHT_PX) / 2;
-      var options = [
+    _createLoginPopup() {
+      const left = window.screenLeft +
+          (window.outerWidth - SIGN_IN_WIDTH_PX) / 2;
+      const top = window.screenTop +
+          (window.outerHeight - SIGN_IN_HEIGHT_PX) / 2;
+      const options = [
         'width=' + SIGN_IN_WIDTH_PX,
         'height=' + SIGN_IN_HEIGHT_PX,
         'left=' + left,
@@ -216,14 +227,14 @@
       this.listen(window, 'focus', '_handleWindowFocus');
     },
 
-    _handleCredentialRefreshed: function() {
+    _handleCredentialRefreshed() {
       this.unlisten(window, 'focus', '_handleWindowFocus');
       this._refreshingCredentials = false;
       this._hideAlert();
       this._showAlert('Credentials refreshed.');
     },
 
-    _handleWindowFocus: function() {
+    _handleWindowFocus() {
       this.flushDebouncer('checkLoggedIn');
     },
   });
diff --git a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
index c8743fb..ba10f09 100644
--- a/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
+++ b/polygerrit-ui/app/elements/core/gr-error-manager/gr-error-manager_test.html
@@ -33,69 +33,69 @@
 </test-fixture>
 
 <script>
-  suite('gr-error-manager tests', function() {
-    var element;
-    var sandbox;
+  suite('gr-error-manager tests', () => {
+    let element;
+    let sandbox;
 
-    setup(function() {
+    setup(() => {
       sandbox = sinon.sandbox.create();
       stub('gr-rest-api-interface', {
-        getLoggedIn: function() { return Promise.resolve(true); },
+        getLoggedIn() { return Promise.resolve(true); },
       });
       element = fixture('basic');
     });
 
-    teardown(function() {
+    teardown(() => {
       sandbox.restore();
     });
 
-    test('does not show auth error on 403 by default', function(done) {
-      var showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
-      var responseText = Promise.resolve('server says no.');
+    test('does not show auth error on 403 by default', done => {
+      const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
+      const responseText = Promise.resolve('server says no.');
       element.fire('server-error',
-          {response: {status: 403, text: function() { return responseText; }}}
+          {response: {status: 403, text() { return responseText; }}}
       );
       Promise.all([
         element.$.restAPI.getLoggedIn.lastCall.returnValue,
         responseText,
-      ]).then(function() {
-          assert.isFalse(showAuthErrorStub.calledOnce);
-          done();
+      ]).then(() => {
+        assert.isFalse(showAuthErrorStub.calledOnce);
+        done();
       });
     });
 
-    test('shows auth error on 403 and Authentication required', function(done) {
-      var showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
-      var responseText = Promise.resolve('Authentication required\n');
+    test('shows auth error on 403 and Authentication required', done => {
+      const showAuthErrorStub = sandbox.stub(element, '_showAuthErrorAlert');
+      const responseText = Promise.resolve('Authentication required\n');
       element.fire('server-error',
-          {response: {status: 403, text: function() { return responseText; }}}
+          {response: {status: 403, text() { return responseText; }}}
       );
       Promise.all([
         element.$.restAPI.getLoggedIn.lastCall.returnValue,
         responseText,
-      ]).then(function() {
+      ]).then(() => {
         assert.isTrue(showAuthErrorStub.calledOnce);
         done();
       });
     });
 
-    test('show logged in error', function() {
+    test('show logged in error', () => {
       sandbox.stub(element, '_showAuthErrorAlert');
       element.fire('show-auth-required');
       assert.isTrue(element._showAuthErrorAlert.calledWithExactly(
           'Log in is required to perform that action.', 'Log in.'));
     });
 
-    test('show normal server error', function(done) {
-      var showAlertStub = sandbox.stub(element, '_showAlert');
-      var textSpy = sandbox.spy(function() { return Promise.resolve('ZOMG'); });
+    test('show normal server error', done => {
+      const showAlertStub = sandbox.stub(element, '_showAlert');
+      const textSpy = sandbox.spy(() => { return Promise.resolve('ZOMG'); });
       element.fire('server-error', {response: {status: 500, text: textSpy}});
 
       assert.isTrue(textSpy.called);
       Promise.all([
         element.$.restAPI.getLoggedIn.lastCall.returnValue,
         textSpy.lastCall.returnValue,
-      ]).then(function() {
+      ]).then(() => {
         assert.isTrue(showAlertStub.calledOnce);
         assert.isTrue(showAlertStub.lastCall.calledWithExactly(
             'Server error: ZOMG'));
@@ -103,9 +103,9 @@
       });
     });
 
-    test('suppress TOO_MANY_FILES error', function(done) {
-      var showAlertStub = sandbox.stub(element, '_showAlert');
-      var textSpy = sandbox.spy(function() {
+    test('suppress TOO_MANY_FILES error', done => {
+      const showAlertStub = sandbox.stub(element, '_showAlert');
+      const textSpy = sandbox.spy(() => {
         return Promise.resolve('too many files to find conflicts');
       });
       element.fire('server-error', {response: {status: 500, text: textSpy}});
@@ -114,17 +114,17 @@
       Promise.all([
         element.$.restAPI.getLoggedIn.lastCall.returnValue,
         textSpy.lastCall.returnValue,
-      ]).then(function() {
+      ]).then(() => {
         assert.isFalse(showAlertStub.called);
         done();
       });
     });
 
-    test('show network error', function(done) {
-      var consoleErrorStub = sandbox.stub(console, 'error');
-      var showAlertStub = sandbox.stub(element, '_showAlert');
+    test('show network error', done => {
+      const consoleErrorStub = sandbox.stub(console, 'error');
+      const showAlertStub = sandbox.stub(element, '_showAlert');
       element.fire('network-error', {error: new Error('ZOMG')});
-      flush(function() {
+      flush(() => {
         assert.isTrue(showAlertStub.calledOnce);
         assert.isTrue(showAlertStub.lastCall.calledWithExactly(
             'Server unavailable'));
@@ -134,21 +134,21 @@
       });
     });
 
-    test('show auth refresh toast', function(done) {
-      var refreshStub = sandbox.stub(element.$.restAPI, 'checkCredentials',
-          function() { return Promise.resolve(true); });
-      var toastSpy = sandbox.spy(element, '_createToastAlert');
-      var windowOpen = sandbox.stub(window, 'open');
-      var responseText = Promise.resolve('Authentication required\n');
+    test('show auth refresh toast', done => {
+      const refreshStub = sandbox.stub(element.$.restAPI, 'checkCredentials',
+          () => { return Promise.resolve(true); });
+      const toastSpy = sandbox.spy(element, '_createToastAlert');
+      const windowOpen = sandbox.stub(window, 'open');
+      const responseText = Promise.resolve('Authentication required\n');
       element.fire('server-error',
-          {response: {status: 403, text: function() { return responseText; }}}
+          {response: {status: 403, text() { return responseText; }}}
       );
       Promise.all([
         element.$.restAPI.getLoggedIn.lastCall.returnValue,
         responseText,
-      ]).then(function() {
+      ]).then(() => {
         assert.isTrue(toastSpy.called);
-        var toast = toastSpy.lastCall.returnValue;
+        let toast = toastSpy.lastCall.returnValue;
         assert.isOk(toast);
         assert.include(
             Polymer.dom(toast.root).textContent, 'Auth error');
@@ -163,12 +163,12 @@
         assert.equal(windowOpen.lastCall.args[2].indexOf('noopener=yes'),
             -1);
 
-        var hideToastSpy = sandbox.spy(toast, 'hide');
+        const hideToastSpy = sandbox.spy(toast, 'hide');
 
         element._handleWindowFocus();
         assert.isTrue(refreshStub.called);
         element.flushDebouncer('checkLoggedIn');
-        flush(function() {
+        flush(() => {
           assert.isTrue(refreshStub.called);
           assert.isTrue(hideToastSpy.called);
 
@@ -182,18 +182,18 @@
       });
     });
 
-    test('show alert', function() {
-      var alertObj = {message: 'foo'}
+    test('show alert', () => {
+      const alertObj = {message: 'foo'};
       sandbox.stub(element, '_showAlert');
-      element.fire('show-alert', {message: 'foo'});
+      element.fire('show-alert', alertObj);
       assert.isTrue(element._showAlert.calledOnce);
       assert.equal(element._showAlert.lastCall.args[0], 'foo');
       assert.isNotOk(element._showAlert.lastCall.args[1]);
       assert.isNotOk(element._showAlert.lastCall.args[2]);
     });
 
-    test('checks stale credentials on visibility change', function() {
-      var refreshStub = sandbox.stub(element.$.restAPI,
+    test('checks stale credentials on visibility change', () => {
+      const refreshStub = sandbox.stub(element.$.restAPI,
           'checkCredentials');
       sandbox.stub(Date, 'now').returns(999999);
       element._lastCredentialCheck = 0;
@@ -211,19 +211,19 @@
       assert.equal(element._lastCredentialCheck, 999999);
     });
 
-    test('refresh loop continues on credential fail', function(done) {
-      var accountPromise = Promise.resolve(null);
+    test('refresh loop continues on credential fail', done => {
+      const accountPromise = Promise.resolve(null);
       sandbox.stub(element.$.restAPI, 'checkCredentials')
           .returns(accountPromise);
-      var requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
-      var handleRefreshStub = sandbox.stub(element,
+      const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
+      const handleRefreshStub = sandbox.stub(element,
           '_handleCredentialRefreshed');
-      var reloadStub = sandbox.stub(element, '_reloadPage');
+      const reloadStub = sandbox.stub(element, '_reloadPage');
 
       element._refreshingCredentials = true;
       element._checkSignedIn();
 
-      accountPromise.then(function() {
+      accountPromise.then(() => {
         assert.isTrue(requestCheckStub.called);
         assert.isFalse(handleRefreshStub.called);
         assert.isFalse(reloadStub.called);
@@ -231,20 +231,20 @@
       });
     });
 
-    test('refreshes with same credentials', function(done) {
-      var accountPromise = Promise.resolve({_account_id: 1234});
+    test('refreshes with same credentials', done => {
+      const accountPromise = Promise.resolve({_account_id: 1234});
       sandbox.stub(element.$.restAPI, 'checkCredentials')
           .returns(accountPromise);
-      var requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
-      var handleRefreshStub = sandbox.stub(element,
+      const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
+      const handleRefreshStub = sandbox.stub(element,
           '_handleCredentialRefreshed');
-      var reloadStub = sandbox.stub(element, '_reloadPage');
+      const reloadStub = sandbox.stub(element, '_reloadPage');
 
       element.knownAccountId = 1234;
       element._refreshingCredentials = true;
       element._checkSignedIn();
 
-      accountPromise.then(function() {
+      accountPromise.then(() => {
         assert.isFalse(requestCheckStub.called);
         assert.isTrue(handleRefreshStub.called);
         assert.isFalse(reloadStub.called);
@@ -252,25 +252,41 @@
       });
     });
 
-    test('reloads when refreshed credentials differ', function(done) {
-      var accountPromise = Promise.resolve({_account_id: 1234});
+    test('reloads when refreshed credentials differ', done => {
+      const accountPromise = Promise.resolve({_account_id: 1234});
       sandbox.stub(element.$.restAPI, 'checkCredentials')
           .returns(accountPromise);
-      var requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
-      var handleRefreshStub = sandbox.stub(element,
+      const requestCheckStub = sandbox.stub(element, '_requestCheckLoggedIn');
+      const handleRefreshStub = sandbox.stub(element,
           '_handleCredentialRefreshed');
-      var reloadStub = sandbox.stub(element, '_reloadPage');
+      const reloadStub = sandbox.stub(element, '_reloadPage');
 
       element.knownAccountId = 4321; // Different from 1234
       element._refreshingCredentials = true;
       element._checkSignedIn();
 
-      accountPromise.then(function() {
+      accountPromise.then(() => {
         assert.isFalse(requestCheckStub.called);
         assert.isFalse(handleRefreshStub.called);
         assert.isTrue(reloadStub.called);
         done();
       });
     });
+
+    test('dismissOnNavigation respected', () => {
+      const asyncStub = sandbox.stub(element, 'async');
+      const hideSpy = sandbox.spy(element, '_hideAlert');
+      // No async call when dismissOnNavigation supplied.
+      element._showAlert('test', null, null, true);
+      assert.isFalse(asyncStub.called);
+
+      // When page nav happens, clear alert.
+      document.dispatchEvent(new CustomEvent('location-change'));
+      assert.isTrue(hideSpy.called);
+
+      // When timeout is not supplied, use HIDE_ALERT_TIMEOUT_MS.
+      element._showAlert('test');
+      assert.isTrue(asyncStub.called);
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index 265e970..0473d66 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -40,6 +40,19 @@
       .bigTitle:hover {
         text-decoration: underline;
       }
+      .bigTitle::before {
+        background-image: var(--header-icon);
+        background-size: var(--header-icon-size) var(--header-icon-size);
+        content: "";
+        display: inline-block;
+        height: var(--header-icon-size);
+        margin: 0 .25em 0 0;
+        vertical-align: text-bottom;
+        width: var(--header-icon-size);
+      }
+      .bigTitle::after {
+        content: var(--header-title-content);
+      }
       ul {
         list-style: none;
       }
@@ -101,7 +114,7 @@
       }
     </style>
     <nav>
-      <a href$="[[_computeRelativeURL('/')]]" class="bigTitle">PolyGerrit</a>
+      <a href$="[[_computeRelativeURL('/')]]" class="bigTitle"></a>
       <ul class="links">
         <template is="dom-repeat" items="[[_links]]" as="linkGroup">
           <li>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index ab042c4..ec57629 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -111,6 +111,7 @@
       },
       _docBaseUrl: {
         type: String,
+        value: null,
       },
       _links: {
         type: Array,
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 1f96014..2296567 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -15,7 +15,7 @@
   'use strict';
 
   // Latency reporting constants.
-  var TIMING = {
+  const TIMING = {
     TYPE: 'timing-report',
     CATEGORY: 'UI Latency',
     // Reported events - alphabetize below.
@@ -24,25 +24,25 @@
   };
 
   // Navigation reporting constants.
-  var NAVIGATION = {
+  const NAVIGATION = {
     TYPE: 'nav-report',
     CATEGORY: 'Location Changed',
     PAGE: 'Page',
   };
 
-  var ERROR = {
+  const ERROR = {
     TYPE: 'error',
     CATEGORY: 'exception',
   };
 
-  var INTERACTION_TYPE = 'interaction';
+  const INTERACTION_TYPE = 'interaction';
 
-  var CHANGE_VIEW_REGEX = /^\/c\/\d+\/?\d*$/;
-  var DIFF_VIEW_REGEX = /^\/c\/\d+\/\d+\/.+$/;
+  const CHANGE_VIEW_REGEX = /^\/c\/\d+\/?\d*$/;
+  const DIFF_VIEW_REGEX = /^\/c\/\d+\/\d+\/.+$/;
 
-  var pending = [];
+  const pending = [];
 
-  var onError = function(oldOnError, msg, url, line, column, error) {
+  const onError = function(oldOnError, msg, url, line, column, error) {
     if (oldOnError) {
       oldOnError(msg, url, line, column, error);
     }
@@ -51,23 +51,23 @@
       column = column || error.columnNumber;
       msg = msg || error.toString();
     }
-    var payload = {
-      url: url,
-      line: line,
-      column: column,
-      error: error,
+    const payload = {
+      url,
+      line,
+      column,
+      error,
     };
     GrReporting.prototype.reporter(ERROR.TYPE, ERROR.CATEGORY, msg, payload);
     return true;
   };
 
-  var catchErrors = function(opt_context) {
-    var context = opt_context || window;
+  const catchErrors = function(opt_context) {
+    const context = opt_context || window;
     context.onerror = onError.bind(null, context.onerror);
   };
   catchErrors();
 
-  var GrReporting = Polymer({
+  const GrReporting = Polymer({
     is: 'gr-reporting',
 
     properties: {
@@ -75,7 +75,7 @@
 
       _baselines: {
         type: Array,
-        value: function() { return {}; },
+        value() { return {}; },
       },
     },
 
@@ -87,24 +87,24 @@
       return window.performance.timing;
     },
 
-    now: function() {
+    now() {
       return Math.round(10 * window.performance.now()) / 10;
     },
 
-    reporter: function() {
-      var report = (Gerrit._arePluginsLoaded() && !pending.length) ?
+    reporter(...args) {
+      const report = (Gerrit._arePluginsLoaded() && !pending.length) ?
         this.defaultReporter : this.cachingReporter;
-      report.apply(this, arguments);
+      report.apply(this, args);
     },
 
-    defaultReporter: function(type, category, eventName, eventValue) {
-      var detail = {
-        type: type,
-        category: category,
+    defaultReporter(type, category, eventName, eventValue) {
+      const detail = {
+        type,
+        category,
         name: eventName,
         value: eventValue,
       };
-      document.dispatchEvent(new CustomEvent(type, {detail: detail}));
+      document.dispatchEvent(new CustomEvent(type, {detail}));
       if (type === ERROR.TYPE) {
         console.error(eventValue.error || eventName);
       } else {
@@ -113,15 +113,15 @@
       }
     },
 
-    cachingReporter: function(type, category, eventName, eventValue) {
+    cachingReporter(type, category, eventName, eventValue) {
       if (type === ERROR.TYPE) {
         console.error(eventValue.error || eventName);
       }
       if (Gerrit._arePluginsLoaded()) {
         if (pending.length) {
-          pending.splice(0).forEach(function(args) {
-            this.reporter.apply(this, args);
-          }, this);
+          for (const args of pending.splice(0)) {
+            this.reporter(...args);
+          }
         }
         this.reporter(type, category, eventName, eventValue);
       } else {
@@ -132,8 +132,8 @@
     /**
      * User-perceived app start time, should be reported when the app is ready.
      */
-    appStarted: function() {
-      var startTime =
+    appStarted() {
+      const startTime =
           new Date().getTime() - this.performanceTiming.navigationStart;
       this.reporter(
           TIMING.TYPE, TIMING.CATEGORY, TIMING.APP_STARTED, startTime);
@@ -142,22 +142,22 @@
     /**
      * Page load time, should be reported at any time after navigation.
      */
-    pageLoaded: function() {
+    pageLoaded() {
       if (this.performanceTiming.loadEventEnd === 0) {
         console.error('pageLoaded should be called after window.onload');
         this.async(this.pageLoaded, 100);
       } else {
-        var loadTime = this.performanceTiming.loadEventEnd -
+        const loadTime = this.performanceTiming.loadEventEnd -
             this.performanceTiming.navigationStart;
         this.reporter(
-          TIMING.TYPE, TIMING.CATEGORY, TIMING.PAGE_LOADED, loadTime);
+            TIMING.TYPE, TIMING.CATEGORY, TIMING.PAGE_LOADED, loadTime);
       }
     },
 
-    locationChanged: function() {
-      var page = '';
-      var pathname = this._getPathname();
-      if (pathname.indexOf('/q/') === 0) {
+    locationChanged() {
+      let page = '';
+      const pathname = this._getPathname();
+      if (pathname.startsWith('/q/')) {
         page = this.getBaseUrl() + '/q/';
       } else if (pathname.match(CHANGE_VIEW_REGEX)) { // change view
         page = this.getBaseUrl() + '/c/';
@@ -171,32 +171,32 @@
           NAVIGATION.TYPE, NAVIGATION.CATEGORY, NAVIGATION.PAGE, page);
     },
 
-    pluginsLoaded: function() {
+    pluginsLoaded() {
       this.timeEnd('PluginsLoaded');
     },
 
-    _getPathname: function() {
+    _getPathname() {
       return '/' + window.location.pathname.substring(this.getBaseUrl().length);
     },
 
     /**
      * Reset named timer.
      */
-    time: function(name) {
+    time(name) {
       this._baselines[name] = this.now();
     },
 
     /**
      * Finish named timer and report it to server.
      */
-    timeEnd: function(name) {
-      var baseTime = this._baselines[name] || 0;
-      var time = this.now() - baseTime;
+    timeEnd(name) {
+      const baseTime = this._baselines[name] || 0;
+      const time = this.now() - baseTime;
       this.reporter(TIMING.TYPE, TIMING.CATEGORY, name, time);
       delete this._baselines[name];
     },
 
-    reportInteraction: function(eventName, opt_msg) {
+    reportInteraction(eventName, opt_msg) {
       this.reporter(INTERACTION_TYPE, this.category, eventName, opt_msg);
     },
   });
@@ -204,5 +204,4 @@
   window.GrReporting = GrReporting;
   // Expose onerror installation so it would be accessible from tests.
   window.GrReporting._catchErrors = 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 2720ebd..bda03b1 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
@@ -32,15 +32,15 @@
 </test-fixture>
 
 <script>
-  suite('gr-reporting tests', function() {
-    var element;
-    var sandbox;
-    var clock;
-    var fakePerformance;
+  suite('gr-reporting tests', () => {
+    let element;
+    let sandbox;
+    let clock;
+    let fakePerformance;
 
-    var NOW_TIME = 100;
+    const NOW_TIME = 100;
 
-    setup(function() {
+    setup(() => {
       sandbox = sinon.sandbox.create();
       clock = sinon.useFakeTimers(NOW_TIME);
       element = fixture('basic');
@@ -49,15 +49,15 @@
         loadEventEnd: 2,
       };
       sinon.stub(element, 'performanceTiming',
-          {get: function() {return fakePerformance;}});
+          {get() {return fakePerformance;}});
       sandbox.stub(element, 'reporter');
     });
-    teardown(function() {
+    teardown(() => {
       sandbox.restore();
       clock.restore();
     });
 
-    test('appStarted', function() {
+    test('appStarted', () => {
       element.appStarted();
       assert.isTrue(
           element.reporter.calledWithExactly(
@@ -66,7 +66,7 @@
       ));
     });
 
-    test('pageLoaded', function() {
+    test('pageLoaded', () => {
       element.pageLoaded();
       assert.isTrue(
           element.reporter.calledWithExactly(
@@ -75,8 +75,8 @@
       );
     });
 
-    test('time and timeEnd', function() {
-      var nowStub = sandbox.stub(element, 'now').returns(0);
+    test('time and timeEnd', () => {
+      const nowStub = sandbox.stub(element, 'now').returns(0);
       element.time('foo');
       nowStub.returns(1);
       element.time('bar');
@@ -92,14 +92,14 @@
       ));
     });
 
-    suite('plugins', function() {
-      setup(function() {
+    suite('plugins', () => {
+      setup(() => {
         element.reporter.restore();
         sandbox.stub(element, 'defaultReporter');
         sandbox.stub(Gerrit, '_arePluginsLoaded');
       });
 
-      test('pluginsLoaded reports time', function() {
+      test('pluginsLoaded reports time', () => {
         Gerrit._arePluginsLoaded.returns(true);
         sandbox.stub(element, 'now').returns(42);
         element.pluginsLoaded();
@@ -108,19 +108,19 @@
         ));
       });
 
-      test('caches reports if plugins are not loaded', function() {
+      test('caches reports if plugins are not loaded', () => {
         Gerrit._arePluginsLoaded.returns(false);
         element.timeEnd('foo');
         assert.isFalse(element.defaultReporter.called);
       });
 
-      test('reports if plugins are loaded', function() {
+      test('reports if plugins are loaded', () => {
         Gerrit._arePluginsLoaded.returns(true);
         element.timeEnd('foo');
         assert.isTrue(element.defaultReporter.called);
       });
 
-      test('reports cached events preserving order', function() {
+      test('reports cached events preserving order', () => {
         Gerrit._arePluginsLoaded.returns(false);
         element.timeEnd('foo');
         Gerrit._arePluginsLoaded.returns(true);
@@ -134,38 +134,38 @@
       });
     });
 
-    suite('location changed', function() {
-      var pathnameStub;
-      setup(function() {
+    suite('location changed', () => {
+      let pathnameStub;
+      setup(() => {
         pathnameStub = sinon.stub(element, '_getPathname');
       });
 
-      teardown(function() {
+      teardown(() => {
         pathnameStub.restore();
       });
 
-      test('search', function() {
+      test('search', () => {
         pathnameStub.returns('/q/foo');
         element.locationChanged();
         assert.isTrue(element.reporter.calledWithExactly(
             'nav-report', 'Location Changed', 'Page', '/q/'));
       });
 
-      test('change view', function() {
+      test('change view', () => {
         pathnameStub.returns('/c/42/');
         element.locationChanged();
         assert.isTrue(element.reporter.calledWithExactly(
             'nav-report', 'Location Changed', 'Page', '/c/'));
       });
 
-      test('change view', function() {
+      test('change view', () => {
         pathnameStub.returns('/c/41/2');
         element.locationChanged();
         assert.isTrue(element.reporter.calledWithExactly(
             'nav-report', 'Location Changed', 'Page', '/c/'));
       });
 
-      test('diff view', function() {
+      test('diff view', () => {
         pathnameStub.returns('/c/41/2/file.txt');
         element.locationChanged();
         assert.isTrue(element.reporter.calledWithExactly(
@@ -173,36 +173,35 @@
       });
     });
 
-    suite('exception logging', function() {
-      var fakeWindow;
-      var reporter;
+    suite('exception logging', () => {
+      let fakeWindow;
+      let reporter;
 
-      var emulateThrow = function(msg, url, line, column, error) {
+      const emulateThrow = function(msg, url, line, column, error) {
         return fakeWindow.onerror(msg, url, line, column, error);
       };
 
-      setup(function() {
+      setup(() => {
         reporter = sandbox.stub(GrReporting.prototype, 'reporter');
         fakeWindow = {};
         sandbox.stub(console, 'error');
         window.GrReporting._catchErrors(fakeWindow);
       });
 
-      test('is reported', function() {
-        var error = new Error('bar');
+      test('is reported', () => {
+        const error = new Error('bar');
         emulateThrow('bar', 'http://url', 4, 2, error);
-        assert.isTrue(
-            reporter.calledWith('error', 'exception', 'bar'));
-        var payload = reporter.lastCall.args[3];
+        assert.isTrue(reporter.calledWith('error', 'exception', 'bar'));
+        const payload = reporter.lastCall.args[3];
         assert.deepEqual(payload, {
           url: 'http://url',
           line: 4,
           column: 2,
-          error: error,
+          error,
         });
       });
 
-      test('prevent default event handler', function() {
+      test('prevent default event handler', () => {
         assert.isTrue(emulateThrow());
       });
     });
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
index d4e7ae2..4edf1be 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar.js
@@ -82,6 +82,7 @@
   ];
 
   var SELF_EXPRESSION = 'self';
+  var ME_EXPRESSION = 'me';
 
   var MAX_AUTOCOMPLETE_RESULTS = 10;
 
@@ -176,9 +177,13 @@
           }).then(function(accounts) {
             // When the expression supplied is a beginning substring of 'self',
             // add it as an autocomplete option.
-            return SELF_EXPRESSION.indexOf(expression) === 0 ?
-                accounts.concat([predicate + ':' + SELF_EXPRESSION]) :
-                accounts;
+            if (SELF_EXPRESSION.indexOf(expression) === 0) {
+              return accounts.concat([predicate + ':' + SELF_EXPRESSION]);
+            } else if (ME_EXPRESSION.indexOf(expression) === 0) {
+              return accounts.concat([predicate + ':' + ME_EXPRESSION]);
+            } else {
+              return accounts;
+            }
           });
     },
 
diff --git a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
index 3ddc96b..05ceb34 100644
--- a/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
+++ b/polygerrit-ui/app/elements/core/gr-search-bar/gr-search-bar_test.html
@@ -160,6 +160,17 @@
         });
       });
 
+      test('Inserts me as option when valid', function(done) {
+        element._getSearchSuggestions('owner:m').then(function(s) {
+          assert.equal(s[0].value, 'owner:me');
+        }).then(function() {
+          element._getSearchSuggestions('owner:meme').then(function(s) {
+            assert.notEqual(s[0].value, 'owner:me');
+            done();
+          });
+        });
+      });
+
       test('Autocompletes groups', function(done) {
         element._getSearchSuggestions('ownerin:pol').then(function(s) {
           assert.equal(s[0].value, 'ownerin:Polygerrit');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
index 214454a..c407829 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder.js
@@ -405,7 +405,6 @@
     td.classList.add(line.type);
     var html = this._escapeHTML(text);
     html = this._addTabWrappers(html, this._prefs.tab_size);
-
     if (!this._prefs.line_wrapping &&
         this._textLength(text, this._prefs.tab_size) >
         this._prefs.line_length) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index 950a046..76d2a69 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -264,7 +264,8 @@
             <option value="SIDE_BY_SIDE">Side By Side</option>
             <option value="UNIFIED_DIFF">Unified</option>
           </select>
-          <span hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]">
+          <span id="diffPrefsContainer"
+              hidden$="[[_computePrefsButtonHidden(_prefs, _loggedIn)]]" hidden>
             <span class="preferences desktop">
               <span
                   hidden$="[[_computeModeSelectHidden(_isImageDiff)]]">/</span>
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
index 33f11c5..f0f814f 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.js
@@ -15,6 +15,7 @@
   'use strict';
 
   var COMMIT_MESSAGE_PATH = '/COMMIT_MSG';
+  var MERGE_LIST_PATH = '/MERGE_LIST';
 
   var COMMENT_SAVE = 'Try again when all comments have saved.';
 
@@ -544,12 +545,16 @@
     },
 
     _computeFileDisplayName: function(path) {
-      return path === COMMIT_MESSAGE_PATH ? 'Commit message' : path;
+      if (path === COMMIT_MESSAGE_PATH) {
+        return 'Commit message';
+      } else if (path === MERGE_LIST_PATH) {
+        return 'Merge list';
+      }
+      return path;
     },
 
     _computeTruncatedFileDisplayName: function(path) {
-      return path === COMMIT_MESSAGE_PATH ?
-          'Commit message' : util.truncatePath(path);
+      return util.truncatePath(this._computeFileDisplayName(path));
     },
 
     _computeFileSelected: function(path, currentPath) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
index d54f715..663cd3b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.html
@@ -252,6 +252,38 @@
           'Should navigate to /c/42/1');
     });
 
+    test('Diff preferences hidden when no prefs or logged out',
+        function() {
+      element._loggedIn = false;
+      flushAsynchronousOperations();
+      assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+      element._loggedIn = true;
+      flushAsynchronousOperations();
+      assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+      element._loggedIn = false;
+      element._prefs = {'font_size': '12'};
+      flushAsynchronousOperations();
+      assert.isTrue(element.$.diffPrefsContainer.hidden);
+
+      element._loggedIn = true;
+      flushAsynchronousOperations();
+      assert.isFalse(element.$.diffPrefsContainer.hidden);
+    });
+
+    test('prefsButton opens gr-diff-preferences', function() {
+      var handlePrefsTapSpy = sandbox.spy(element, '_handlePrefsTap');
+      var overlayOpenStub = sandbox.stub(element.$.diffPreferences,
+          'open');
+      var prefsButton = Polymer.dom(element.root).querySelector('.prefsButton');
+
+      MockInteractions.tap(prefsButton);
+
+      assert.isTrue(handlePrefsTapSpy.called);
+      assert.isTrue(overlayOpenStub.called);
+    });
+
     test('go up to change via kb without change loaded', function() {
       element._changeNum = '42';
       element._patchRange = {
@@ -327,6 +359,8 @@
           '/foo/bar/baz');
       assert.equal(element._computeFileDisplayName('/COMMIT_MSG'),
           'Commit message');
+      assert.equal(element._computeFileDisplayName('/MERGE_LIST'),
+          'Merge list');
     });
 
     test('jump to file dropdown with patch range', function() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index d560723..ba402f2 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -72,6 +72,7 @@
         type: Boolean,
         reflectToAttribute: true,
       },
+      noRenderOnPrefsChange: Boolean,
       _loggedIn: {
         type: Boolean,
         value: false,
@@ -436,7 +437,7 @@
 
       this.updateStyles();
 
-      if (this._diff && this._comments) {
+      if (this._diff && this._comments && !this.noRenderOnPrefsChange) {
         this._renderDiffTable();
       }
     },
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
index 811a7e9..f765b26 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_test.html
@@ -837,6 +837,59 @@
         });
       });
 
+      suite('change in preferences', function() {
+        setup(function() {
+          element._diff = {
+            meta_a: {name: 'carrot.jpg', content_type: 'image/jpeg', lines: 66},
+            meta_b: {name: 'carrot.jpg', content_type: 'image/jpeg',
+                lines: 560},
+            diff_header: [],
+            intraline_status: 'OK',
+            change_type: 'MODIFIED',
+            content: [{skip: 66}],
+          };
+          element._comments = {
+            meta: {
+              changeNum: '42',
+              patchRange: {
+                basePatchNum: 'PARENT',
+                patchNum: 3,
+              },
+              path: '/path/to/foo',
+              projectConfig: {foo: 'bar'},
+            },
+            left: [
+              {id: 'bc1', side: 'PARENT', __commentSide: 'left'},
+              {id: 'bc2', side: 'PARENT', __commentSide: 'left'},
+              {id: 'bd1', __draft: true, side: 'PARENT', __commentSide: 'left'},
+              {id: 'bd2', __draft: true, side: 'PARENT', __commentSide: 'left'},
+            ],
+            right: [
+              {id: 'c1', __commentSide: 'right'},
+              {id: 'c2', __commentSide: 'right'},
+              {id: 'd1', __draft: true, __commentSide: 'right'},
+              {id: 'd2', __draft: true, __commentSide: 'right'},
+            ],
+          };
+        });
+
+        test('change in preferences re-renders diff', function() {
+          sandbox.stub(element, '_renderDiffTable');
+          element.prefs = {};
+          element.prefs = {time_format: 'HHMM_12'};
+          assert.isTrue(element._renderDiffTable.called);
+        });
+
+        test('change in preferences does not re-renders diff with ' +
+            'noRenderOnPrefsChange', function() {
+          sandbox.stub(element, '_renderDiffTable');
+          element.noRenderOnPrefsChange = true;
+          element.prefs = {};
+          element.prefs = {time_format: 'HHMM_12'};
+          assert.isFalse(element._renderDiffTable.called);
+        });
+      });
+
       suite('handle comment-update', function() {
 
         setup(function() {
diff --git a/polygerrit-ui/app/elements/gr-app.html b/polygerrit-ui/app/elements/gr-app.html
index 6f09701..87a4687 100644
--- a/polygerrit-ui/app/elements/gr-app.html
+++ b/polygerrit-ui/app/elements/gr-app.html
@@ -43,7 +43,7 @@
     <style>
       :host {
         display: flex;
-        min-height: 100vh;
+        min-height: 100%;
         flex-direction: column;
       }
       gr-main-header,
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
index 0e64cb2..91bc628 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info.js
@@ -79,7 +79,7 @@
     },
 
     save: function() {
-      if (!this.mutable || !this.hasUnsavedChanges) {
+      if (!this.hasUnsavedChanges) {
         return Promise.resolve();
       }
 
@@ -97,9 +97,9 @@
     },
 
     _maybeSetName: function() {
-      return this._hasNameChange ?
-          this.$.restAPI.setAccountName(this._account.name) :
-          Promise.resolve();
+      return this._hasNameChange && this.mutable ?
+                this.$.restAPI.setAccountName(this._account.name) :
+                Promise.resolve();
     },
 
     _maybeSetStatus: function() {
diff --git a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
index 8f4e89d..cf35450 100644
--- a/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-account-info/gr-account-info_test.html
@@ -168,5 +168,92 @@
         });
       });
     });
+
+    suite('edit name and status', function() {
+      var nameChangedSpy;
+      var statusChangedSpy;
+      var nameStub;
+      var statusStub;
+
+      setup(function() {
+        nameChangedSpy = sandbox.spy(element, '_nameChanged');
+        statusChangedSpy = sandbox.spy(element, '_statusChanged');
+        element.set('_serverConfig',
+          {auth: {editable_account_fields: ['FULL_NAME']}});
+
+        nameStub = sandbox.stub(element.$.restAPI, 'setAccountName',
+            function(name) { return Promise.resolve(); });
+        statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
+            function(status) { return Promise.resolve(); });
+      });
+
+      test('set name and status', function(done) {
+        assert.isTrue(element.mutable);
+        assert.isFalse(element.hasUnsavedChanges);
+
+        element.set('_account.name', 'new name');
+
+        assert.isTrue(nameChangedSpy.called);
+
+        element.set('_account.status', 'new status');
+
+        assert.isTrue(statusChangedSpy.called);
+
+        assert.isTrue(element.hasUnsavedChanges);
+
+        element.save().then(function() {
+          assert.isTrue(statusStub.called);
+          assert.isTrue(nameStub.called);
+
+          assert.equal(nameStub.lastCall.args[0], 'new name');
+
+          assert.equal(statusStub.lastCall.args[0], 'new status');
+
+          done();
+        });
+      });
+    });
+
+    suite('set status but read name', function() {
+      var statusChangedSpy;
+      var statusStub;
+
+      setup(function() {
+        statusChangedSpy = sandbox.spy(element, '_statusChanged');
+        element.set('_serverConfig',
+          {auth: {editable_account_fields: []}});
+
+        statusStub = sandbox.stub(element.$.restAPI, 'setAccountStatus',
+            function(status) { return Promise.resolve(); });
+      });
+
+      test('read full name but set status', function(done) {
+        var section = element.$.nameSection;
+        var displaySpan = section.querySelectorAll('.value')[0];
+        var inputSpan = section.querySelectorAll('.value')[1];
+
+        assert.isFalse(element.mutable);
+
+        assert.isFalse(element.hasUnsavedChanges);
+
+        assert.isFalse(displaySpan.hasAttribute('hidden'));
+        assert.equal(displaySpan.textContent, account.name);
+        assert.isTrue(inputSpan.hasAttribute('hidden'));
+
+        element.set('_account.status', 'new status');
+
+        assert.isTrue(statusChangedSpy.called);
+
+        assert.isTrue(element.hasUnsavedChanges);
+
+        element.save().then(function() {
+          assert.isTrue(statusStub.called);
+          statusStub.lastCall.returnValue.then(function() {
+            assert.equal(statusStub.lastCall.args[0], 'new status');
+            done();
+          });
+        });
+      });
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index e3aea75..f485dd6 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -118,11 +118,10 @@
         <fieldset id="profile">
           <gr-account-info
               id="accountInfo"
-              mutable="{{_accountInfoMutable}}"
+              mutable="{{_accountNameMutable}}"
               has-unsaved-changes="{{_accountInfoChanged}}"></gr-account-info>
           <gr-button
               on-tap="_handleSaveAccountInfo"
-              hidden$="[[!_accountInfoMutable]]"
               disabled="[[!_accountInfoChanged]]">Save changes</gr-button>
         </fieldset>
         <h2
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 4c852b5..4647a2d 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -48,7 +48,7 @@
         type: Object,
         value: function() { return {}; },
       },
-      _accountInfoMutable: Boolean,
+      _accountNameMutable: Boolean,
       _accountInfoChanged: Boolean,
       _diffPrefs: Object,
       _changeTableColumnsNotDisplayed: Array,
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
index 9d6b83b..aeb9607 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api.js
@@ -22,8 +22,8 @@
     this._el.setLabelValue(label, value);
   };
 
-  GrChangeReplyInterface.prototype.send = function() {
-    return this._el.send();
+  GrChangeReplyInterface.prototype.send = function(opt_includeComments) {
+    return this._el.send(opt_includeComments);
   };
 
   window.GrChangeReplyInterface = GrChangeReplyInterface;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
index d7d5cfe..c2357cc 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-change-reply-js-api_test.html
@@ -66,8 +66,8 @@
       assert(setLabelValueStub.calledWithExactly('My-Label', '+1337'));
 
       var sendStub = sinon.stub(element, 'send');
-      changeReply.send();
-      assert(sendStub.calledWithExactly());
+      changeReply.send(false);
+      assert(sendStub.calledWithExactly(false));
     });
   });
 </script>
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 7b0ce19..ae42c2a 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
@@ -1102,5 +1102,24 @@
           return response.ok;
         });
     },
+
+    startWorkInProgress: function(changeNum, opt_message) {
+      var payload = {};
+      if (opt_message) {
+        payload.message = opt_message;
+      }
+      var url = this.getChangeActionURL(changeNum, null, '/wip');
+      return this.send('POST', url, payload)
+          .then(function(response) {
+            if (response.status === 204) {
+              return 'Change marked as Work In Progress.';
+            }
+          });
+    },
+
+    startReview: function(changeNum, review) {
+      return this.send(
+          'POST', this.getChangeActionURL(changeNum, null, '/ready'), review);
+    },
   });
 })();
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
index f2f41b8..933b4f4 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface_test.html
@@ -613,5 +613,22 @@
           }
       );
     });
+
+    test('startWorkInProgress', function() {
+      sandbox.stub(element, 'send').returns(Promise.resolve('ok'));
+      element.startWorkInProgress('42');
+      assert.isTrue(element.send.calledWith(
+          'POST', '/changes/42/wip', {}));
+      element.startWorkInProgress('42', 'revising...');
+      assert.isTrue(element.send.calledWith(
+          'POST', '/changes/42/wip', {message: 'revising...'}));
+    });
+
+    test('startReview', function() {
+      sandbox.stub(element, 'send').returns(Promise.resolve({}));
+      element.startReview('42', {message: 'Please review.'});
+      assert.isTrue(element.send.calledWith(
+          'POST', '/changes/42/ready', {message: 'Please review.'}));
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/test/plugin.html b/polygerrit-ui/app/elements/test/plugin.html
index 0314655..4efd6cf 100644
--- a/polygerrit-ui/app/elements/test/plugin.html
+++ b/polygerrit-ui/app/elements/test/plugin.html
@@ -11,6 +11,7 @@
     html {
       --primary-text-color: #F00BAA;
       --header-background-color: #F01BAA;
+      --header-title-content: "MyGerrit";
       --footer-background-color: #F02BAA;
     }
   </style>
diff --git a/polygerrit-ui/app/lint_test.sh b/polygerrit-ui/app/lint_test.sh
new file mode 100755
index 0000000..7ee74d8
--- /dev/null
+++ b/polygerrit-ui/app/lint_test.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+set -ex
+
+eslint_bin=$(which npm)
+if [[ -z "$eslint_bin" ]]; then
+    echo "NPM must be on the path."
+    exit 1
+fi
+
+eslint_bin=$(which eslint)
+eslint_config=$(npm list -g | grep -c eslint-config-google)
+eslint_plugin=$(npm list -g | grep -c eslint-plugin-html)
+if [[ -z "$eslint_bin" ]] || [[ eslint_config -eq "0" ]] || [[ eslint_plugin -eq "0" ]]; then
+    echo "You must install ESLint and its dependencies from NPM."
+    echo "> npm install -g eslint eslint-config-google eslint-plugin-html"
+    echo "For more information, view the README:"
+    echo "https://gerrit.googlesource.com/gerrit/+/master/polygerrit-ui/#Style-guide"
+    exit 1
+fi
+
+${eslint_bin} --ignore-pattern 'bower_components/' --ignore-pattern 'gr-linked-text' --ignore-pattern 'scripts/vendor' --ext .html,.js .
diff --git a/polygerrit-ui/app/styles/app-theme.html b/polygerrit-ui/app/styles/app-theme.html
index e88b70c..c998f03 100644
--- a/polygerrit-ui/app/styles/app-theme.html
+++ b/polygerrit-ui/app/styles/app-theme.html
@@ -18,6 +18,9 @@
   /* Following vars have LTS for plugin API. */
   --primary-text-color: #000;
   --header-background-color: #eee;
+  --header-title-content: 'PolyGerrit';
+  --header-icon: none;
+  --header-icon-size: 0em;
   --footer-background-color: var(--header-background-color);
 
   /* Following are not part of plugin API. */
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl
index 2dbeae7..55bfae1 100644
--- a/tools/bzl/maven_jar.bzl
+++ b/tools/bzl/maven_jar.bzl
@@ -64,12 +64,13 @@
       formatted_deps += "    ],"
   return formatted_deps
 
-def _generate_build_file(ctx, binjar, srcjar):
+def _generate_build_files(ctx, binjar, srcjar):
+  header = "# DO NOT EDIT: automatically generated BUILD file for maven_jar rule %s" % ctx.name
   srcjar_attr = ""
   if srcjar:
     srcjar_attr = 'srcjar = "%s",' % srcjar
   contents = """
-# DO NOT EDIT: automatically generated BUILD file for maven_jar rule {rule_name}
+{header}
 package(default_visibility = ['//visibility:public'])
 java_import(
     name = 'jar',
@@ -86,10 +87,10 @@
     {exports}
 )
 \n""".format(srcjar_attr = srcjar_attr,
-              rule_name = ctx.name,
-              binjar = binjar,
-              deps = _format_deps("deps", ctx.attr.deps),
-              exports = _format_deps("exports", ctx.attr.exports))
+             header = header,
+             binjar = binjar,
+             deps = _format_deps("deps", ctx.attr.deps),
+             exports = _format_deps("exports", ctx.attr.exports))
   if srcjar:
     contents += """
 java_import(
@@ -99,6 +100,18 @@
 """.format(srcjar = srcjar)
   ctx.file('%s/BUILD' % ctx.path("jar"), contents, False)
 
+  # Compatibility layer for java_import_external from rules_closure
+  contents = """
+{header}
+package(default_visibility = ['//visibility:public'])
+
+alias(
+    name = "{rule_name}",
+    actual = "@{rule_name}//jar",
+)
+\n""".format(rule_name = ctx.name, header = header)
+  ctx.file("BUILD", contents, False)
+
 def _maven_jar_impl(ctx):
   """rule to download a Maven archive."""
   coordinates = _create_coordinates(ctx.attr.artifact)
@@ -142,7 +155,7 @@
     if out.return_code:
       fail("failed %s: %s" % (args, out.stderr))
 
-  _generate_build_file(ctx, binjar, srcjar)
+  _generate_build_files(ctx, binjar, srcjar)
 
 maven_jar = repository_rule(
     attrs = {
diff --git a/tools/coverage.sh b/tools/coverage.sh
new file mode 100755
index 0000000..8fa979f
--- /dev/null
+++ b/tools/coverage.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+#
+# Usage
+#
+#   COVERAGE_CPUS=32 tools/coverage.sh [/path/to/report-directory/]
+#
+# COVERAGE_CPUS defaults to 2, and the default destination is a temp
+# dir.
+
+genhtml=$(which genhtml)
+if [[ -z "${genhtml}" ]]; then
+    echo "Install 'genhtml' (contained in the 'lcov' package)"
+    exit 1
+fi
+
+destdir="$1"
+if [[ -z "${destdir}" ]]; then
+    destdir=$(mktemp -d /tmp/gerritcov.XXXXXX)
+fi
+
+echo "Running 'bazel coverage'; this may take a while"
+
+# coverage is expensive to run; use --jobs=2 to avoid overloading the
+# machine.
+bazel coverage -k --jobs=${COVERAGE_CPUS:-2} -- ... -//gerrit-common:auto_value_tests
+
+# The coverage data contains filenames relative to the Java root, and
+# genhtml has no logic to search these elsewhere. Workaround this
+# limitation by running genhtml in a directory with the files in the
+# right place. Also -inexplicably- genhtml wants to have the source
+# files relative to the output directory.
+mkdir -p ${destdir}/
+cp -a */src/{main,test}/java/* ${destdir}/
+
+base=$(bazel info bazel-testlogs)
+for f in $(find ${base}  -name 'coverage.dat') ; do
+  cp $f ${destdir}/$(echo $f| sed "s|${base}/||" | sed "s|/|_|g")
+done
+
+cd ${destdir}
+find -name '*coverage.dat' -size 0 -delete
+
+genhtml -o . --ignore-errors source *coverage.dat
+
+echo "coverage report at file://${destdir}/index.html"