Merge "Rebase: Use committer identity in new commit"
diff --git a/Documentation/config-plugins.txt b/Documentation/config-plugins.txt
index e5614cd..6e248c8 100644
--- a/Documentation/config-plugins.txt
+++ b/Documentation/config-plugins.txt
@@ -413,6 +413,14 @@
 https://github.com/davido/gerrit-oauth-provider[Project] |
 https://github.com/davido/gerrit-oauth-provider/wiki/Getting-Started[Configuration]
 
+[[owners]]
+=== owners
+This plugin provides a Prolog predicate `add_owner_approval/3` that
+appends `label('Owner-Approval', need(_))` to a provided list.
+
+link:https://gerrit-review.googlesource.com/#/admin/projects/plugins/owners[Project] |
+link:https://gerrit.googlesource.com/plugins/owners/+/refs/heads/master/README.md[Documentation]
+
 [[project-download-commands]]
 === project-download-commands
 
diff --git a/Documentation/config-project-config.txt b/Documentation/config-project-config.txt
index 0d3ff58..7d03681 100644
--- a/Documentation/config-project-config.txt
+++ b/Documentation/config-project-config.txt
@@ -164,6 +164,17 @@
 Default is `INHERIT`, which means that this property is inherited from
 the parent project.
 
+[[receive.requireSignedPush]]receive.requireSignedPush::
++
+Controls whether server-side signed push validation is required on the
+project. Only has an effect if signed push validation is enabled on the
+server, and link:#receive.enableSignedPush is set on the project. See
+the link:config-gerrit.html#receive.enableSignedPush[global
+configuration] for details.
++
+Default is `INHERIT`, which means that this property is inherited from
+the parent project.
+
 [[submit-section]]
 === Submit section
 
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index 26072e3..e9ba824 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -766,6 +766,17 @@
 can be notified when this configuration parameter is updated on a
 project.
 
+[[configuring-groups]]
+=== Referencing groups in `project.config`
+
+Plugins can refer to groups so that when they are renamed, the project
+config will also be updated in this section. The proper format to use is
+the string representation of a GroupReference, as shown below.
+
+----
+Group[group_name / group_uuid]
+----
+
 [[project-specific-configuration]]
 == Project Specific Configuration in own config file
 
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index a320a54..6920c4c 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -1256,7 +1256,7 @@
   {
     "context": 10,
     "theme": "DEFAULT",
-    "ignore_whitespace": "IGNORE_ALL_SPACE",
+    "ignore_whitespace": "IGNORE_ALL",
     "intraline_difference": true,
     "line_length": 100,
     "show_tabs": true,
@@ -1285,7 +1285,7 @@
   {
     "context": 10,
     "theme": "ECLIPSE",
-    "ignore_whitespace": "IGNORE_ALL_SPACE",
+    "ignore_whitespace": "IGNORE_ALL",
     "intraline_difference": true,
     "line_length": 100,
     "show_line_endings": true,
@@ -1309,7 +1309,7 @@
   {
     "context": 10,
     "theme": "ECLIPSE",
-    "ignore_whitespace": "IGNORE_ALL_SPACE",
+    "ignore_whitespace": "IGNORE_ALL",
     "intraline_difference": true,
     "line_length": 100,
     "show_line_endings": true,
@@ -1666,8 +1666,8 @@
 |`ignore_whitespace`           ||
 Whether whitespace changes should be ignored and if yes, which
 whitespace changes should be ignored. +
-Allowed values are `IGNORE_NONE`, `IGNORE_SPACE_AT_EOL`,
-`IGNORE_SPACE_CHANGE`, `IGNORE_ALL_SPACE`.
+Allowed values are `IGNORE_NONE`, `IGNORE_TRAILING`,
+`IGNORE_LEADING_AND_TRAILING`, `IGNORE_ALL`.
 |`intraline_difference`        |not set if `false`|
 Whether intraline differences should be highlighted.
 |`line_length`                 ||
@@ -1722,8 +1722,8 @@
 |`ignore_whitespace`           |optional|
 Whether whitespace changes should be ignored and if yes, which
 whitespace changes should be ignored. +
-Allowed values are `IGNORE_NONE`, `IGNORE_SPACE_AT_EOL`,
-`IGNORE_SPACE_CHANGE`, `IGNORE_ALL_SPACE`.
+Allowed values are `IGNORE_NONE`, `IGNORE_TRAILING`,
+`IGNORE_LEADING_AND_TRAILING`, `IGNORE_ALL`.
 |`intraline_difference`        |optional|
 Whether intraline differences should be highlighted.
 |`line_length`                 |optional|
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 7e96556..55764b1 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -4482,23 +4482,23 @@
 
 [options="header",cols="1,^1,5"]
 |============================
-|Field Name     ||Description
-|`message`      |optional|
+|Field Name               ||Description
+|`message`                |optional|
 The message to be added as review comment.
-|`labels`       |optional|
+|`labels`                 |optional|
 The votes that should be added to the revision as a map that maps the
 label names to the voting values.
-|`comments`     |optional|
+|`comments`               |optional|
 The comments that should be added as a map that maps a file path to a
 list of link:#comment-input[CommentInput] entities.
-|`strict_labels`|`true` if not set|
+|`strict_labels`          |`true` if not set|
 Whether all labels are required to be within the user's permitted ranges
 based on access controls. +
 If `true`, attempting to use a label not granted to the user will fail
 the entire modify operation early. +
 If `false`, the operation will execute anyway, but the proposed labels
 will be modified to be the "best" value allowed by the access controls.
-|`drafts`      |optional|
+|`drafts`                 |optional|
 Draft handling that defines how draft comments are handled that are
 already in the database but that were not also described in this
 input. +
@@ -4506,12 +4506,14 @@
 `KEEP`. All values except `PUBLISH_ALL_REVISIONS` operate only on drafts
 for a single revision. +
 If not set, the default is `DELETE`.
-|`notify`      |optional|
+|`notify`                 |optional|
 Notify handling that defines to whom email notifications should be sent
 after the review is stored. +
 Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
 If not set, the default is `ALL`.
-|`on_behalf_of`|optional|
+|`omit_duplicate_comments`|optional|
+If `true`, comments with the same content at the same place will be omitted.
+|`on_behalf_of`           |optional|
 link:rest-api-accounts.html#account-id[\{account-id\}] the review
 should be posted on behalf of. To use this option the caller must
 have been granted `labelAs-NAME` permission for all keys of labels.
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 7ae320d..59184a8 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -732,6 +732,7 @@
     "use_signed_off_by": "INHERIT",
     "create_new_change_for_all_not_in_target": "INHERIT",
     "enable_signed_push": "INHERIT",
+    "require_signed_push": "INHERIT",
     "require_change_id": "TRUE",
     "max_object_size_limit": "10m",
     "submit_type": "REBASE_IF_NECESSARY",
@@ -780,6 +781,11 @@
       "configured_value": "INHERIT",
       "inherited_value": false
     },
+    "require_signed_push": {
+      "value": false,
+      "configured_value": "INHERIT",
+      "inherited_value": false
+    },
     "max_object_size_limit": {
       "value": "10m",
       "configured_value": "10m",
@@ -1982,9 +1988,14 @@
 valid link:user-changeid.html[Change-Id] footer in any commit uploaded
 for review is required. This does not apply to commits pushed directly
 to a branch or tag.
-|`enable_signed_push`                      |optional|
+|`enable_signed_push`|
+optional, not set if signed push is disabled|
 link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
 signed push validation is enabled on the project.
+|`require_signed_push`|
+optional, not set if signed push is disabled
+link:#inherited-boolean-info[InheritedBooleanInfo] that tells whether
+signed push validation is required on the project.
 |`max_object_size_limit`     ||
 The link:config-gerrit.html#receive.maxObjectSizeLimit[max object size
 limit] of this project as a link:#max-object-size-limit-info[
diff --git a/Documentation/user-inline-edit.txt b/Documentation/user-inline-edit.txt
index fff2501..05932df 100644
--- a/Documentation/user-inline-edit.txt
+++ b/Documentation/user-inline-edit.txt
@@ -59,6 +59,8 @@
 To save edits, click the 'Save' button or press `CTRL-S`.  To return to the
 change screen, click the 'Close' button.
 
+Note that when editing the commit message, trailing blank lines will be stripped.
+
 image::images/inline-edit-full-screen-editor.png[width=800, link="images/inline-edit-full-screen-editor.png"]
 
 If there are unsaved edits when the 'Close' button is pressed, a dialog will
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 45e5c22..39296d0 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
@@ -19,6 +19,10 @@
 import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.gpg.PublicKeyStore.REFS_GPG_KEYS;
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static com.google.gerrit.gpg.testutil.TestKeys.allValidKeys;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithSecondUserId;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.common.base.Function;
@@ -38,7 +42,6 @@
 import com.google.gerrit.gpg.PublicKeyStore;
 import com.google.gerrit.gpg.server.GpgKeys;
 import com.google.gerrit.gpg.testutil.TestKey;
-import com.google.gerrit.gpg.testutil.TestKeys;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.server.config.AllUsersName;
@@ -204,7 +207,7 @@
 
   @Test
   public void addGpgKey() throws Exception {
-    TestKey key = TestKeys.key1();
+    TestKey key = validKeyWithoutExpiration();
     String id = key.getKeyIdString();
     addExternalIdEmail(admin, "test1@example.com");
 
@@ -220,7 +223,7 @@
   @Test
   public void reAddExistingGpgKey() throws Exception {
     addExternalIdEmail(admin, "test5@example.com");
-    TestKey key = TestKeys.key5();
+    TestKey key = validKeyWithSecondUserId();
     String id = key.getKeyIdString();
     PGPPublicKey pk = key.getPublicKey();
 
@@ -243,7 +246,7 @@
 
     db.accountExternalIds().insert(Collections.singleton(extId));
 
-    TestKey key = TestKeys.key5();
+    TestKey key = validKeyWithSecondUserId();
     addGpgKey(key.getPublicKeyArmored());
     setApiUser(user);
 
@@ -254,7 +257,7 @@
 
   @Test
   public void listGpgKeys() throws Exception {
-    List<TestKey> keys = TestKeys.allValidKeys();
+    List<TestKey> keys = allValidKeys();
     List<String> toAdd = new ArrayList<>(keys.size());
     for (TestKey key : keys) {
       addExternalIdEmail(admin,
@@ -267,7 +270,7 @@
 
   @Test
   public void deleteGpgKey() throws Exception {
-    TestKey key = TestKeys.key1();
+    TestKey key = validKeyWithoutExpiration();
     String id = key.getKeyIdString();
     addExternalIdEmail(admin, "test1@example.com");
     addGpgKey(key.getPublicKeyArmored());
@@ -283,13 +286,13 @@
 
   @Test
   public void addAndRemoveGpgKeys() throws Exception {
-    for (TestKey key : TestKeys.allValidKeys()) {
+    for (TestKey key : allValidKeys()) {
       addExternalIdEmail(admin,
           PushCertificateIdent.parse(key.getFirstUserId()).getEmailAddress());
     }
-    TestKey key1 = TestKeys.key1();
-    TestKey key2 = TestKeys.key2();
-    TestKey key5 = TestKeys.key5();
+    TestKey key1 = validKeyWithoutExpiration();
+    TestKey key2 = validKeyWithExpiration();
+    TestKey key5 = validKeyWithSecondUserId();
 
     Map<String, GpgKeyInfo> infos = gApi.accounts().self().putGpgKeys(
         ImmutableList.of(
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 3d24c71..a9293f6 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
@@ -29,6 +29,7 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.TestProjectInput;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.api.changes.AddReviewerInput;
@@ -38,6 +39,7 @@
 import com.google.gerrit.extensions.client.ListChangesOption;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -53,6 +55,7 @@
 import org.junit.Test;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Set;
@@ -124,6 +127,45 @@
         .id(r.getChangeId())
         .revision(r.getCommit().name())
         .submit();
+    ChangeInfo revertChange =
+        gApi.changes()
+            .id(r.getChangeId())
+            .revert().get();
+
+    // expected messages on source change:
+    // 1. Uploaded patch set 1.
+    // 2. Patch Set 1: Code-Review+2
+    // 3. Change has been successfully merged by Administrator
+    // 4. Patch Set 1: Reverted
+    List<ChangeMessageInfo> sourceMessages = new ArrayList<>(
+        gApi.changes().id(r.getChangeId()).get().messages);
+    assertThat(sourceMessages).hasSize(4);
+    String expectedMessage = String.format(
+        "Patch Set 1: Reverted\n\n" +
+        "This patchset was reverted in change: %s",
+        revertChange.changeId);
+    assertThat(sourceMessages.get(3).message).isEqualTo(expectedMessage);
+
+    assertThat(revertChange.messages).hasSize(1);
+    assertThat(revertChange.messages.iterator().next().message)
+        .isEqualTo("Uploaded patch set 1.");
+  }
+
+  @Test
+  @TestProjectInput(createEmptyCommit = false)
+  public void revertInitialCommit() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .submit();
+
+    exception.expect(ResourceConflictException.class);
+    exception.expectMessage("Cannot revert initial commit");
     gApi.changes()
         .id(r.getChangeId())
         .revert();
@@ -140,6 +182,31 @@
   }
 
   @Test
+  public void rebaseConflict() throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .submit();
+
+    PushOneCommit push = pushFactory.create(db, admin.getIdent(), testRepo,
+        PushOneCommit.SUBJECT, PushOneCommit.FILE_NAME, "other content",
+        "If09d8782c1e59dd0b33de2b1ec3595d69cc10ad5");
+    r = push.to("refs/for/master");
+    r.assertOkStatus();
+
+    exception.expect(ResourceConflictException.class);
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .rebase();
+  }
+
+  @Test
   public void rebaseChangeBase() throws Exception {
     PushOneCommit.Result r1 = createChange();
     PushOneCommit.Result r2 = createChange();
@@ -448,16 +515,23 @@
     assertThat(actual.revisions.get(r1.getCommit().getName()).commitWithFooters)
         .isNull();
 
-    String expected = SUBJECT + "\n"
-        + "\n"
-        + "Change-Id: " + r2.getChangeId() + "\n"
-        + "Reviewed-on: "
-            + canonicalWebUrl.get() + r2.getChange().getId() + "\n"
-        + "Reviewed-by: Administrator <admin@example.com>\n"
-        + "Custom2: Administrator <admin@example.com>\n"
-        + "Tested-by: Administrator <admin@example.com>\n";
-    assertThat(actual.revisions.get(r2.getCommit().getName()).commitWithFooters)
-        .isEqualTo(expected);
+    List<String> footers =
+        new ArrayList<>(Arrays.asList(
+            actual.revisions.get(r2.getCommit().getName())
+            .commitWithFooters.split("\\n")));
+    // remove subject + blank line
+    footers.remove(0);
+    footers.remove(0);
+
+    List<String> expectedFooters = Arrays.asList(
+        "Change-Id: " + r2.getChangeId(),
+        "Reviewed-on: "
+            + canonicalWebUrl.get() + r2.getChange().getId(),
+        "Reviewed-by: Administrator <admin@example.com>",
+        "Custom2: Administrator <admin@example.com>",
+        "Tested-by: Administrator <admin@example.com>");
+
+    assertThat(footers).containsExactlyElementsIn(expectedFooters);
   }
 
   @Test
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index 7ecf1f3..3e970e4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -18,8 +18,8 @@
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
 import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
 import static com.google.gerrit.acceptance.PushOneCommit.PATCH;
-import static org.eclipse.jgit.lib.Constants.HEAD;
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.eclipse.jgit.lib.Constants.HEAD;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
index 625b33a..ed20e24 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/edit/ChangeEditIT.java
@@ -318,7 +318,7 @@
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertThat(edit.get().getEditCommit().getParentCount()).isEqualTo(0);
 
-    String msg = String.format("New commit message\n\nChange-Id: %s",
+    String msg = String.format("New commit message\n\nChange-Id: %s\n",
         change.getKey());
     assertThat(modifier.modifyMessage(edit.get(), msg))
         .isEqualTo(RefUpdate.Result.FORCED);
@@ -345,8 +345,9 @@
     assertThat(modifier.createEdit(change, getCurrentPatchSet(changeId)))
         .isEqualTo(RefUpdate.Result.NEW);
     Optional<ChangeEdit> edit = editUtil.byChange(change);
-
-    String msg = String.format("New commit message\n\nChange-Id: %s",
+    assertUnchangedMessage(edit, edit.get().getEditCommit().getFullMessage());
+    assertUnchangedMessage(edit, edit.get().getEditCommit().getFullMessage() + "\n\n");
+    String msg = String.format("New commit message\n\nChange-Id: %s\n",
         change.getKey());
     assertThat(modifier.modifyMessage(edit.get(), msg)).isEqualTo(
         RefUpdate.Result.FORCED);
@@ -373,7 +374,7 @@
         .isEqualTo(SC_NOT_FOUND);
     EditMessage.Input in = new EditMessage.Input();
     in.message = String.format("New commit message\n\n" +
-        CONTENT_NEW2_STR + "\n\nChange-Id: %s",
+        CONTENT_NEW2_STR + "\n\nChange-Id: %s\n",
         change.getKey());
     assertThat(adminSession.put(urlEditMessage(), in).getStatusCode())
         .isEqualTo(SC_NO_CONTENT);
@@ -383,7 +384,7 @@
     Optional<ChangeEdit> edit = editUtil.byChange(change);
     assertThat(edit.get().getEditCommit().getFullMessage())
         .isEqualTo(in.message);
-    in.message = String.format("New commit message2\n\nChange-Id: %s",
+    in.message = String.format("New commit message2\n\nChange-Id: %s\n",
         change.getKey());
     assertThat(adminSession.put(urlEditMessage(), in).getStatusCode())
         .isEqualTo(SC_NO_CONTENT);
@@ -712,6 +713,14 @@
     assertThat(approvals.get(0).value).isEqualTo(1);
   }
 
+  private void assertUnchangedMessage(Optional<ChangeEdit> edit, String message)
+      throws Exception {
+    exception.expect(UnchangedCommitMessageException.class);
+    exception.expectMessage(
+        "New commit message cannot be same as existing commit message");
+    modifier.modifyMessage(edit.get(), message);
+  }
+
   @Test
   public void testHasEditPredicate() throws Exception {
     assertThat(modifier.createEdit(change, ps)).isEqualTo(RefUpdate.Result.NEW);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/DiffPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/DiffPreferencesIT.java
new file mode 100644
index 0000000..5550cb6
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/DiffPreferencesIT.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.account;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
+import com.google.gerrit.extensions.client.Theme;
+import com.google.gerrit.testutil.ConfigSuite;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Test;
+
+public class DiffPreferencesIT extends AbstractDaemonTest {
+  @ConfigSuite.Config
+  public static Config readFromGitConfig() {
+    Config cfg = new Config();
+    cfg.setBoolean("user", null, "readPrefsFromGit", true);
+    return cfg;
+  }
+
+  @Test
+  public void getDiffPreferencesOfNonExistingAccount_NotFound()
+      throws Exception {
+    assertEquals(HttpStatus.SC_NOT_FOUND,
+        adminSession.get("/accounts/non-existing/preferences.diff")
+        .getStatusCode());
+  }
+
+  @Test
+  public void getDiffPreferences() throws Exception {
+    RestResponse r = adminSession.get("/accounts/" + admin.email
+        + "/preferences.diff");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    DiffPreferencesInfo d = DiffPreferencesInfo.defaults();
+    DiffPreferencesInfo o =
+        newGson().fromJson(r.getReader(), DiffPreferencesInfo.class);
+
+    assertThat(o.context).isEqualTo(d.context);
+    assertThat(o.tabSize).isEqualTo(d.tabSize);
+    assertThat(o.lineLength).isEqualTo(d.lineLength);
+    assertThat(o.expandAllComments).isNull();
+    assertThat(o.intralineDifference).isEqualTo(d.intralineDifference);
+    assertThat(o.manualReview).isNull();
+    assertThat(o.retainHeader).isNull();
+    assertThat(o.showLineEndings).isEqualTo(d.showLineEndings);
+    assertThat(o.showTabs).isEqualTo(d.showTabs);
+    assertThat(o.showWhitespaceErrors).isEqualTo(d.showWhitespaceErrors);
+    assertThat(o.skipDeleted).isNull();
+    assertThat(o.skipUncommented).isNull();
+    assertThat(o.syntaxHighlighting).isEqualTo(d.syntaxHighlighting);
+    assertThat(o.hideTopMenu).isNull();
+    assertThat(o.autoHideDiffTableHeader).isEqualTo(d.autoHideDiffTableHeader);
+    assertThat(o.hideLineNumbers).isNull();
+    assertThat(o.renderEntireFile).isNull();
+    assertThat(o.hideEmptyPane).isNull();
+    assertThat(o.ignoreWhitespace).isEqualTo(d.ignoreWhitespace);
+    assertThat(o.theme).isEqualTo(d.theme);
+  }
+
+  @Test
+  public void setDiffPreferences() throws Exception {
+    DiffPreferencesInfo i = DiffPreferencesInfo.defaults();
+
+    // change all default values
+    i.context *= -1;
+    i.tabSize *= -1;
+    i.lineLength *= -1;
+    i.theme = Theme.MIDNIGHT;
+    i.ignoreWhitespace = Whitespace.IGNORE_ALL;
+    i.expandAllComments ^= true;
+    i.intralineDifference ^= true;
+    i.manualReview ^= true;
+    i.retainHeader ^= true;
+    i.showLineEndings ^= true;
+    i.showTabs ^= true;
+    i.showWhitespaceErrors ^= true;
+    i.skipDeleted ^= true;
+    i.skipUncommented ^= true;
+    i.syntaxHighlighting ^= true;
+    i.hideTopMenu ^= true;
+    i.autoHideDiffTableHeader ^= true;
+    i.hideLineNumbers ^= true;
+    i.renderEntireFile ^= true;
+    i.hideEmptyPane ^= true;
+
+    RestResponse r = adminSession.put("/accounts/" + admin.email
+        + "/preferences.diff", i);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    DiffPreferencesInfo o = newGson().fromJson(r.getReader(),
+        DiffPreferencesInfo.class);
+
+    assertThat(o.context).isEqualTo(i.context);
+    assertThat(o.tabSize).isEqualTo(i.tabSize);
+    assertThat(o.lineLength).isEqualTo(i.lineLength);
+    assertThat(o.expandAllComments).isEqualTo(i.expandAllComments);
+    assertThat(o.intralineDifference).isNull();
+    assertThat(o.manualReview).isEqualTo(i.manualReview);
+    assertThat(o.retainHeader).isEqualTo(i.retainHeader);
+    assertThat(o.showLineEndings).isNull();
+    assertThat(o.showTabs).isNull();
+    assertThat(o.showWhitespaceErrors).isNull();
+    assertThat(o.skipDeleted).isEqualTo(i.skipDeleted);
+    assertThat(o.skipUncommented).isEqualTo(i.skipUncommented);
+    assertThat(o.syntaxHighlighting).isNull();
+    assertThat(o.hideTopMenu).isEqualTo(i.hideTopMenu);
+    assertThat(o.autoHideDiffTableHeader).isNull();
+    assertThat(o.hideLineNumbers).isEqualTo(i.hideLineNumbers);
+    assertThat(o.renderEntireFile).isEqualTo(i.renderEntireFile);
+    assertThat(o.hideEmptyPane).isEqualTo(i.hideEmptyPane);
+    assertThat(o.ignoreWhitespace).isEqualTo(i.ignoreWhitespace);
+    assertThat(o.theme).isEqualTo(i.theme);
+
+    // Partially fill input record
+    i = new DiffPreferencesInfo();
+    i.tabSize = 42;
+    r = adminSession.put("/accounts/" + admin.email
+        + "/preferences.diff", i);
+    DiffPreferencesInfo a = newGson().fromJson(r.getReader(),
+        DiffPreferencesInfo.class);
+
+    assertThat(a.context).isEqualTo(o.context);
+    assertThat(a.tabSize).isEqualTo(42);
+    assertThat(a.lineLength).isEqualTo(o.lineLength);
+    assertThat(a.expandAllComments).isEqualTo(o.expandAllComments);
+    assertThat(a.intralineDifference).isNull();
+    assertThat(a.manualReview).isEqualTo(o.manualReview);
+    assertThat(a.retainHeader).isEqualTo(o.retainHeader);
+    assertThat(a.showLineEndings).isNull();
+    assertThat(a.showTabs).isNull();
+    assertThat(a.showWhitespaceErrors).isNull();
+    assertThat(a.skipDeleted).isEqualTo(o.skipDeleted);
+    assertThat(a.skipUncommented).isEqualTo(o.skipUncommented);
+    assertThat(a.syntaxHighlighting).isNull();
+    assertThat(a.hideTopMenu).isEqualTo(o.hideTopMenu);
+    assertThat(a.autoHideDiffTableHeader).isNull();
+    assertThat(a.hideLineNumbers).isEqualTo(o.hideLineNumbers);
+    assertThat(a.renderEntireFile).isEqualTo(o.renderEntireFile);
+    assertThat(a.hideEmptyPane).isEqualTo(o.hideEmptyPane);
+    assertThat(a.ignoreWhitespace).isEqualTo(o.ignoreWhitespace);
+    assertThat(a.theme).isEqualTo(o.theme);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java
index 8770c3c..cf89c5a 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/EditPreferencesIT.java
@@ -69,6 +69,18 @@
     assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
     EditPreferencesInfo info = getEditPrefInfo(r);
     assertEditPreferences(info, out);
+
+    // Partially filled input record
+    EditPreferencesInfo in = new EditPreferencesInfo();
+    in.tabSize = 42;
+    r = adminSession.put(endPoint, in);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
+
+    r = adminSession.get(endPoint);
+    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
+    info = getEditPrefInfo(r);
+    out.tabSize = in.tabSize;
+    assertEditPreferences(info, out);
   }
 
   private EditPreferencesInfo getEditPrefInfo(RestResponse r)
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetDiffPreferencesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetDiffPreferencesIT.java
deleted file mode 100644
index 02a94f8..0000000
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetDiffPreferencesIT.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.acceptance.rest.account;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.server.account.GetDiffPreferences.DiffPreferencesInfo;
-
-import org.apache.http.HttpStatus;
-import org.junit.Test;
-
-public class GetDiffPreferencesIT extends AbstractDaemonTest {
-  @Test
-  public void getDiffPreferencesOfNonExistingAccount_NotFound()
-      throws Exception {
-    assertThat(adminSession.get("/accounts/non-existing/preferences.diff").getStatusCode())
-      .isEqualTo(HttpStatus.SC_NOT_FOUND);
-  }
-
-  @Test
-  public void getDiffPreferences() throws Exception {
-    RestResponse r = adminSession.get("/accounts/" + admin.email + "/preferences.diff");
-    assertThat(r.getStatusCode()).isEqualTo(HttpStatus.SC_OK);
-    DiffPreferencesInfo diffPreferences =
-        newGson().fromJson(r.getReader(), DiffPreferencesInfo.class);
-    assertDiffPreferences(new AccountDiffPreference(admin.id), diffPreferences);
-  }
-
-  private static void assertDiffPreferences(AccountDiffPreference expected, DiffPreferencesInfo actual) {
-    assertThat(actual.context).isEqualTo(expected.getContext());
-    assertThat(toBoolean(actual.expandAllComments)).isEqualTo(expected.isExpandAllComments());
-    assertThat(actual.ignoreWhitespace).isEqualTo(expected.getIgnoreWhitespace());
-    assertThat(toBoolean(actual.intralineDifference)).isEqualTo(expected.isIntralineDifference());
-    assertThat(actual.lineLength).isEqualTo(expected.getLineLength());
-    assertThat(toBoolean(actual.manualReview)).isEqualTo(expected.isManualReview());
-    assertThat(toBoolean(actual.retainHeader)).isEqualTo(expected.isRetainHeader());
-    assertThat(toBoolean(actual.showLineEndings)).isEqualTo(expected.isShowLineEndings());
-    assertThat(toBoolean(actual.showTabs)).isEqualTo(expected.isShowTabs());
-    assertThat(toBoolean(actual.showWhitespaceErrors)).isEqualTo(expected.isShowWhitespaceErrors());
-    assertThat(toBoolean(actual.skipDeleted)).isEqualTo(expected.isSkipDeleted());
-    assertThat(toBoolean(actual.skipUncommented)).isEqualTo(expected.isSkipUncommented());
-    assertThat(toBoolean(actual.syntaxHighlighting)).isEqualTo(expected.isSyntaxHighlighting());
-    assertThat(actual.tabSize).isEqualTo(expected.getTabSize());
-  }
-
-  private static boolean toBoolean(Boolean b) {
-    if (b == null) {
-      return false;
-    }
-    return b.booleanValue();
-  }
-}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
index 621d94a..2707507 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DraftChangeIT.java
@@ -82,10 +82,12 @@
     ChangeInfo c = get(triplet);
     assertThat(c.id).isEqualTo(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.DRAFT);
+    assertThat(c.revisions.get(c.currentRevision).draft).isTrue();
     RestResponse response = publishChange(changeId);
     assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SC_NO_CONTENT);
     c = get(triplet);
     assertThat(c.status).isEqualTo(ChangeStatus.NEW);
+    assertThat(c.revisions.get(c.currentRevision).draft).isNull();
   }
 
   @Test
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 5592755..b784f05 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
@@ -189,11 +189,34 @@
   }
 
   @Test
+  public void addDuplicateComments() throws Exception {
+    PushOneCommit.Result r1 = createChange();
+    String changeId = r1.getChangeId();
+    String revId = r1.getCommit().getName();
+    addComment(r1, "nit: trailing whitespace");
+    addComment(r1, "nit: trailing whitespace");
+    Map<String, List<CommentInfo>> result = getPublishedComments(changeId, revId);
+    assertThat(result.get(FILE_NAME)).hasSize(2);
+    addComment(r1, "nit: trailing whitespace", true);
+    result = getPublishedComments(changeId, revId);
+    assertThat(result.get(FILE_NAME)).hasSize(2);
+
+    PushOneCommit.Result r2 = pushFactory.create(
+          db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "content")
+        .to("refs/for/master");
+    changeId = r2.getChangeId();
+    revId = r2.getCommit().getName();
+    addComment(r2, "nit: trailing whitespace", true);
+    result = getPublishedComments(changeId, revId);
+    assertThat(result.get(FILE_NAME)).hasSize(1);
+  }
+
+  @Test
   public void listChangeDrafts() throws Exception {
     PushOneCommit.Result r1 = createChange();
 
     PushOneCommit.Result r2 = pushFactory.create(
-          db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new cntent",
+          db, admin.getIdent(), testRepo, SUBJECT, FILE_NAME, "new content",
           r1.getChangeId())
         .to("refs/for/master");
 
@@ -363,9 +386,13 @@
         + "-- \n");
   }
 
-
   private void addComment(PushOneCommit.Result r, String message)
       throws Exception {
+    addComment(r, message, false);
+  }
+
+  private void addComment(PushOneCommit.Result r, String message,
+      boolean omitDuplicateComments) throws Exception {
     CommentInput c = new CommentInput();
     c.line = 1;
     c.message = message;
@@ -373,6 +400,7 @@
     ReviewInput in = new ReviewInput();
     in.comments = ImmutableMap.<String, List<CommentInput>> of(
         FILE_NAME, ImmutableList.of(c));
+    in.omitDuplicateComments = omitDuplicateComments;
     gApi.changes()
         .id(r.getChangeId())
         .revision(r.getCommit().name())
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index 42f3fe7..2f110de 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -30,11 +30,9 @@
 import com.google.gerrit.server.edit.ChangeEditModifier;
 import com.google.gerrit.server.edit.ChangeEditUtil;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.junit.Test;
@@ -43,20 +41,6 @@
 import java.util.List;
 
 public class GetRelatedIT extends AbstractDaemonTest {
-  @ConfigSuite.Default
-  public static Config byGroup() {
-    Config cfg = new Config();
-    cfg.setBoolean("change", null, "getRelatedByAncestors", false);
-    return cfg;
-  }
-
-  @ConfigSuite.Config
-  public static Config byAncestors() {
-    Config cfg = new Config();
-    cfg.setBoolean("change", null, "getRelatedByAncestors", true);
-    return cfg;
-  }
-
   @Inject
   private ChangeEditUtil editUtil;
 
@@ -347,8 +331,8 @@
 
     // 1,2 is related directly to 4,1, and the 2-3 parallel branch stays intact.
     assertRelated(ps1_2,
-        changeAndCommit(ps3_2, c3_2, 2),
         changeAndCommit(ps4_1, c4_1, 1),
+        changeAndCommit(ps3_2, c3_2, 2),
         changeAndCommit(ps2_2, c2_2, 2),
         changeAndCommit(ps1_2, c1_2, 2));
 
@@ -433,6 +417,91 @@
   }
 
   @Test
+  public void getRelatedParallelDescendentBranches() throws Exception {
+    // 1,1---2,1---3,1
+    //   \---4,1---5,1
+    //    \--6,1---7,1
+
+    RevCommit c1_1 = commitBuilder()
+        .add("a.txt", "1")
+        .message("subject: 1")
+        .create();
+    RevCommit c2_1 = commitBuilder()
+        .add("b.txt", "2")
+        .message("subject: 2")
+        .create();
+    RevCommit c3_1 = commitBuilder()
+        .add("c.txt", "3")
+        .message("subject: 3")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps1_1 = getPatchSetId(c1_1);
+    PatchSet.Id ps2_1 = getPatchSetId(c2_1);
+    PatchSet.Id ps3_1 = getPatchSetId(c3_1);
+
+    testRepo.reset(c1_1);
+    RevCommit c4_1 = commitBuilder()
+        .add("d.txt", "4")
+        .message("subject: 4")
+        .create();
+    RevCommit c5_1 = commitBuilder()
+        .add("e.txt", "5")
+        .message("subject: 5")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps4_1 = getPatchSetId(c4_1);
+    PatchSet.Id ps5_1 = getPatchSetId(c5_1);
+
+    testRepo.reset(c1_1);
+    RevCommit c6_1 = commitBuilder()
+        .add("f.txt", "6")
+        .message("subject: 6")
+        .create();
+    RevCommit c7_1 = commitBuilder()
+        .add("g.txt", "7")
+        .message("subject: 7")
+        .create();
+    pushHead(testRepo, "refs/for/master", false);
+    PatchSet.Id ps6_1 = getPatchSetId(c6_1);
+    PatchSet.Id ps7_1 = getPatchSetId(c7_1);
+
+    // All changes are related to 1,1, keeping each of the parallel branches
+    // intact.
+    assertRelated(ps1_1,
+        changeAndCommit(ps7_1, c7_1, 1),
+        changeAndCommit(ps6_1, c6_1, 1),
+        changeAndCommit(ps5_1, c5_1, 1),
+        changeAndCommit(ps4_1, c4_1, 1),
+        changeAndCommit(ps3_1, c3_1, 1),
+        changeAndCommit(ps2_1, c2_1, 1),
+        changeAndCommit(ps1_1, c1_1, 1));
+
+    // The 2-3 branch is only related back to 1, not the other branches.
+    for (PatchSet.Id ps : ImmutableList.of(ps2_1, ps3_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps3_1, c3_1, 1),
+          changeAndCommit(ps2_1, c2_1, 1),
+          changeAndCommit(ps1_1, c1_1, 1));
+    }
+
+    // The 4-5 branch is only related back to 1, not the other branches.
+    for (PatchSet.Id ps : ImmutableList.of(ps4_1, ps5_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps5_1, c5_1, 1),
+          changeAndCommit(ps4_1, c4_1, 1),
+          changeAndCommit(ps1_1, c1_1, 1));
+    }
+
+    // The 6-7 branch is only related back to 1, not the other branches.
+    for (PatchSet.Id ps : ImmutableList.of(ps6_1, ps7_1)) {
+      assertRelated(ps,
+          changeAndCommit(ps7_1, c7_1, 1),
+          changeAndCommit(ps6_1, c6_1, 1),
+          changeAndCommit(ps1_1, c1_1, 1));
+    }
+  }
+
+  @Test
   public void getRelatedEdit() throws Exception {
     // 1,1---2,1---3,1
     //   \---2,E---/
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
index b80d6d9..c4a07f2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/PatchListCacheIT.java
@@ -20,8 +20,8 @@
 
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gerrit.server.patch.PatchListCache;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
index 9a30696..9bc2ea5 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/IoUtil.java
@@ -26,6 +26,7 @@
 import java.net.URLClassLoader;
 import java.nio.file.Path;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Set;
 
 public final class IoUtil {
@@ -86,8 +87,8 @@
     }
   }
 
-  public static void loadJARs(Path... jars) {
-    loadJARs(Arrays.asList(jars));
+  public static void loadJARs(Path jar) {
+    loadJARs(Collections.singleton(jar));
   }
 
   private static UnsupportedOperationException noAddURL(String m, Throwable why) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
index 81bca20..533dfa2 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
@@ -16,8 +16,8 @@
 
 import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -35,7 +35,7 @@
 
   @Audit
   @SignInRequired
-  void changeDiffPreferences(AccountDiffPreference diffPref,
+  void changeDiffPreferences(DiffPreferencesInfo diffPref,
       AsyncCallback<VoidResult> callback);
 
   @SignInRequired
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
index 5824415..6d9c2cd 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.audit.Audit;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -29,5 +29,5 @@
 
   @Audit
   void patchSetDetail2(PatchSet.Id baseId, PatchSet.Id key,
-      AccountDiffPreference diffPrefs, AsyncCallback<PatchSetDetail> callback);
+      DiffPreferencesInfo diffPrefs, AsyncCallback<PatchSetDetail> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
index c261fdd..3362ba2 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
@@ -27,6 +27,14 @@
     return new GroupReference(group.getGroupUUID(), group.getName());
   }
 
+  public static GroupReference fromString(String ref) {
+    String name =
+        ref.substring(ref.indexOf("[") + 1, ref.lastIndexOf("/")).trim();
+    String uuid =
+        ref.substring(ref.lastIndexOf("/") + 1, ref.lastIndexOf("]")).trim();
+    return new GroupReference(new AccountGroup.UUID(uuid), name);
+  }
+
   protected String uuid;
   protected String name;
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
index aa86c47..2e991d9 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 
 import java.util.Date;
 import java.util.List;
@@ -22,7 +22,7 @@
 /** Data sent as part of the host page, to bootstrap the UI. */
 public class HostPageData {
   public String version;
-  public AccountDiffPreference accountDiffPref;
+  public DiffPreferencesInfo accountDiffPref;
   public String xGerritAuth;
   public Theme theme;
   public List<String> plugins;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
index 19fcbeb..d9601f0 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.audit.Audit;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -27,5 +27,5 @@
 public interface PatchDetailService extends RemoteJsonService {
   @Audit
   void patchScript(Patch.Key key, PatchSet.Id a, PatchSet.Id b,
-      AccountDiffPreference diffPrefs, AsyncCallback<PatchScript> callback);
+      DiffPreferencesInfo diffPrefs, AsyncCallback<PatchScript> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index 7bab43a..f23afb1 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
@@ -42,7 +42,7 @@
   protected FileMode oldMode;
   protected FileMode newMode;
   protected List<String> header;
-  protected AccountDiffPreference diffPrefs;
+  protected DiffPreferencesInfo diffPrefs;
   protected SparseFileContent a;
   protected SparseFileContent b;
   protected List<Edit> edits;
@@ -62,7 +62,7 @@
 
   public PatchScript(final Change.Key ck, final ChangeType ct, final String on,
       final String nn, final FileMode om, final FileMode nm,
-      final List<String> h, final AccountDiffPreference dp,
+      final List<String> h, final DiffPreferencesInfo dp,
       final SparseFileContent ca, final SparseFileContent cb,
       final List<Edit> e, final DisplayMethod ma, final DisplayMethod mb,
       final String mta, final String mtb, final CommentDetail cd,
@@ -142,11 +142,11 @@
     return history;
   }
 
-  public AccountDiffPreference getDiffPrefs() {
+  public DiffPreferencesInfo getDiffPrefs() {
     return diffPrefs;
   }
 
-  public void setDiffPrefs(AccountDiffPreference dp) {
+  public void setDiffPrefs(DiffPreferencesInfo dp) {
     diffPrefs = dp;
   }
 
@@ -155,7 +155,7 @@
   }
 
   public boolean isIgnoreWhitespace() {
-    return diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE;
+    return diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE;
   }
 
   public boolean hasIntralineDifference() {
@@ -171,7 +171,7 @@
   }
 
   public boolean isExpandAllComments() {
-    return diffPrefs.isExpandAllComments();
+    return diffPrefs.expandAllComments;
   }
 
   public SparseFileContent getA() {
@@ -195,8 +195,8 @@
   }
 
   public Iterable<EditList.Hunk> getHunks() {
-    int ctx = diffPrefs.getContext();
-    if (ctx == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+    int ctx = diffPrefs.context;
+    if (ctx == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) {
       ctx = Math.max(a.size(), b.size());
     }
     return new EditList(edits, ctx, a.size(), b.size()).getHunks();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
index 873c560..e6ce6b2 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/api/changes/ReviewInput.java
@@ -49,6 +49,12 @@
   public NotifyHandling notify = NotifyHandling.ALL;
 
   /**
+   * If true check to make sure that the comments being posted aren't already
+   * present.
+   */
+  public boolean omitDuplicateComments;
+
+  /**
    * Account ID, name, email address or username of another user. The review
    * will be posted/updated on behalf of this named user instead of the
    * caller. Caller must have the labelAs-$NAME permission granted for each
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
new file mode 100644
index 0000000..c488bd7
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/DiffPreferencesInfo.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.client;
+
+public class DiffPreferencesInfo {
+
+  /** Default number of lines of context. */
+  public static final int DEFAULT_CONTEXT = 10;
+
+  /** Default tab size. */
+  public static final int DEFAULT_TAB_SIZE = 8;
+
+  /** Default line length. */
+  public static final int DEFAULT_LINE_LENGTH = 100;
+
+  /** Context setting to display the entire file. */
+  public static final short WHOLE_FILE_CONTEXT = -1;
+
+  /** Typical valid choices for the default context setting. */
+  public static final short[] CONTEXT_CHOICES =
+      {3, 10, 25, 50, 75, 100, WHOLE_FILE_CONTEXT};
+
+  public static enum Whitespace {
+    IGNORE_NONE,
+    IGNORE_TRAILING,
+    IGNORE_LEADING_AND_TRAILING,
+    IGNORE_ALL;
+  }
+
+  public Integer context;
+  public Integer tabSize;
+  public Integer lineLength;
+  public Boolean expandAllComments;
+  public Boolean intralineDifference;
+  public Boolean manualReview;
+  public Boolean showLineEndings;
+  public Boolean showTabs;
+  public Boolean showWhitespaceErrors;
+  public Boolean syntaxHighlighting;
+  public Boolean hideTopMenu;
+  public Boolean autoHideDiffTableHeader;
+  public Boolean hideLineNumbers;
+  public Boolean renderEntireFile;
+  public Boolean hideEmptyPane;
+  public Theme theme;
+  public Whitespace ignoreWhitespace;
+  public Boolean retainHeader;
+  public Boolean skipDeleted;
+  public Boolean skipUncommented;
+
+  public static DiffPreferencesInfo defaults() {
+    DiffPreferencesInfo i = new DiffPreferencesInfo();
+    i.context = DEFAULT_CONTEXT;
+    i.tabSize = DEFAULT_TAB_SIZE;
+    i.lineLength = DEFAULT_LINE_LENGTH;
+    i.ignoreWhitespace = Whitespace.IGNORE_NONE;
+    i.theme = Theme.DEFAULT;
+    i.expandAllComments = false;
+    i.intralineDifference = true;
+    i.manualReview = false;
+    i.retainHeader = false;
+    i.showLineEndings = true;
+    i.showTabs = true;
+    i.showWhitespaceErrors = true;
+    i.skipDeleted = false;
+    i.skipUncommented = false;
+    i.syntaxHighlighting = true;
+    i.hideTopMenu = false;
+    i.autoHideDiffTableHeader = true;
+    i.hideLineNumbers = false;
+    i.renderEntireFile = false;
+    i.hideEmptyPane = false;
+    return i;
+  }
+}
\ No newline at end of file
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java
index 5d5af75..3485b8b 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/client/Side.java
@@ -15,5 +15,16 @@
 package com.google.gerrit.extensions.client;
 
 public enum Side {
-  PARENT, REVISION
-}
\ No newline at end of file
+  PARENT,
+  REVISION;
+
+  public static Side fromShort(short s) {
+    switch (s) {
+      case 0:
+        return PARENT;
+      case 1:
+        return REVISION;
+    }
+    return null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MergeConflictException.java
similarity index 74%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MergeConflictException.java
index 02bc8dc..a67db0f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MergeConflictException.java
@@ -12,11 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.extensions.restapi;
 
-/** Indicates that the commit cannot be merged without conflicts. */
-public class MergeConflictException extends Exception {
+/**
+ * Indicates that a commit cannot be merged without conflicts.
+ * <p>
+ * Messages should be viewable by end users.
+ */
+public class MergeConflictException extends ResourceConflictException {
   private static final long serialVersionUID = 1L;
+
   public MergeConflictException(String msg) {
     super(msg, null);
   }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/Fingerprint.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/Fingerprint.java
index 6fd8bac..fa78f01 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/Fingerprint.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/Fingerprint.java
@@ -19,6 +19,9 @@
 import org.eclipse.jgit.util.NB;
 
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 public class Fingerprint {
   private final byte[] fp;
@@ -33,6 +36,18 @@
         NB.decodeUInt16(fp, 16), NB.decodeUInt16(fp, 18));
   }
 
+  public static long getId(byte[] fp) {
+    return NB.decodeInt64(fp, 12);
+  }
+
+  public static Map<Long, Fingerprint> byId(Iterable<Fingerprint> fps) {
+    Map<Long, Fingerprint> result = new HashMap<>();
+    for (Fingerprint fp : fps) {
+      result.put(fp.getId(), fp);
+    }
+    return Collections.unmodifiableMap(result);
+  }
+
   private static byte[] checkLength(byte[] fp) {
     checkArgument(fp.length == 20,
         "fingerprint must be 20 bytes, got %s", fp.length);
@@ -54,6 +69,23 @@
     this.fp = checkLength(fp);
   }
 
+  /**
+   * Wrap a portion of a fingerprint byte array.
+   * <p>
+   * Unlike {@link #Fingerprint(byte[])}, creates a new copy of the byte array.
+   *
+   * @param buf byte array to wrap; must have at least {@code off + 20} bytes.
+   * @param off offset in buf.
+   */
+  public Fingerprint(byte[] buf, int off) {
+    int expected = 20 + off;
+    checkArgument(buf.length >= expected,
+        "fingerprint buffer must have at least %s bytes, got %s",
+        expected, buf.length);
+    this.fp = new byte[20];
+    System.arraycopy(buf, off, fp, 0, 20);
+  }
+
   public byte[] get() {
     return fp;
   }
@@ -79,6 +111,6 @@
   }
 
   public long getId() {
-    return NB.decodeInt64(fp, 12);
+    return getId(fp);
   }
 }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
index d942c75..c3c886f 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPublicKeyChecker.java
@@ -14,14 +14,14 @@
 
 package com.google.gerrit.gpg;
 
-import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GPGKEY;
 
 import com.google.common.base.CharMatcher;
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Ordering;
 import com.google.common.io.BaseEncoding;
 import com.google.gerrit.common.PageLinks;
@@ -44,11 +44,10 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
-import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -61,18 +60,13 @@
   private static final Logger log =
       LoggerFactory.getLogger(GerritPublicKeyChecker.class);
 
-  private final Provider<ReviewDb> db;
-  private final String webUrl;
-  private final IdentifiedUser.GenericFactory userFactory;
-  private final IdentifiedUser expectedUser;
-
   @Singleton
   public static class Factory {
     private final Provider<ReviewDb> db;
     private final String webUrl;
     private final IdentifiedUser.GenericFactory userFactory;
     private final int maxTrustDepth;
-    private final ImmutableList<Fingerprint> trusted;
+    private final ImmutableMap<Long, Fingerprint> trusted;
 
     @Inject
     Factory(@GerritServerConfig Config cfg,
@@ -86,52 +80,58 @@
 
       String[] strs = cfg.getStringList("receive", null, "trustedKey");
       if (strs.length != 0) {
-        List<Fingerprint> fps = new ArrayList<>(strs.length);
+        Map<Long, Fingerprint> fps =
+            Maps.newHashMapWithExpectedSize(strs.length);
         for (String str : strs) {
           str = CharMatcher.WHITESPACE.removeFrom(str).toUpperCase();
-          fps.add(new Fingerprint(BaseEncoding.base16().decode(str)));
+          Fingerprint fp = new Fingerprint(BaseEncoding.base16().decode(str));
+          fps.put(fp.getId(), fp);
         }
-        trusted = ImmutableList.copyOf(fps);
+        trusted = ImmutableMap.copyOf(fps);
       } else {
         trusted = null;
       }
     }
 
-    /**
-     * Create a checker that can check arbitrary public keys.
-     * <p>
-     * Each key is checked against the set of identities in the database
-     * belonging to the same user as the key.
-     *
-     * @return a new checker.
-     */
     public GerritPublicKeyChecker create() {
-      return new GerritPublicKeyChecker(this, null);
+      return new GerritPublicKeyChecker(this);
     }
 
-    /**
-     * Create a checker for checking a single public key against a known user.
-     * <p>
-     * The top-level key passed to {@link #check(PGPPublicKey, PublicKeyStore)}
-     * must belong to the given user. (Other keys checked in the course of
-     * verifying the web of trust are checked against the set of identities in
-     * the database belonging to the same user as the key.)
-     *
-     * @param expectedUser the user
-     * @return a new checker.
-     */
-    public GerritPublicKeyChecker create(IdentifiedUser expectedUser) {
-      checkNotNull(expectedUser);
-      return new GerritPublicKeyChecker(this, expectedUser);
+    public GerritPublicKeyChecker create(IdentifiedUser expectedUser,
+        PublicKeyStore store) {
+      GerritPublicKeyChecker checker = new GerritPublicKeyChecker(this);
+      checker.setExpectedUser(expectedUser);
+      checker.setStore(store);
+      return checker;
     }
   }
 
-  private GerritPublicKeyChecker(Factory factory, IdentifiedUser expectedUser) {
-    super(factory.maxTrustDepth, factory.trusted);
+  private final Provider<ReviewDb> db;
+  private final String webUrl;
+  private final IdentifiedUser.GenericFactory userFactory;
+
+  private IdentifiedUser expectedUser;
+
+  private GerritPublicKeyChecker(Factory factory) {
     this.db = factory.db;
     this.webUrl = factory.webUrl;
     this.userFactory = factory.userFactory;
+    if (factory.trusted != null) {
+      enableTrust(factory.maxTrustDepth, factory.trusted);
+    }
+  }
+
+   /**
+    * Set the expected user for this checker.
+    * <p>
+    * If set, the top-level key passed to {@link #check(PGPPublicKey)} must
+    * belong to the given user. (Other keys checked in the course of verifying
+    * the web of trust are checked against the set of identities in the database
+    * belonging to the same user as the key.)
+    */
+  public GerritPublicKeyChecker setExpectedUser(IdentifiedUser expectedUser) {
     this.expectedUser = expectedUser;
+    return this;
   }
 
   @Override
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java
index fbc3d44..30983ac 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GerritPushCertificateChecker.java
@@ -26,8 +26,7 @@
 
 public class GerritPushCertificateChecker extends PushCertificateChecker {
   public interface Factory {
-    GerritPushCertificateChecker create(IdentifiedUser expectedUser,
-        boolean checkNonce);
+    GerritPushCertificateChecker create(IdentifiedUser expectedUser);
   }
 
   private final GitRepositoryManager repoManager;
@@ -38,9 +37,8 @@
       GerritPublicKeyChecker.Factory keyCheckerFactory,
       GitRepositoryManager repoManager,
       AllUsersName allUsers,
-      @Assisted IdentifiedUser expectedUser,
-      @Assisted boolean checkNonce) {
-    super(keyCheckerFactory.create(expectedUser), checkNonce);
+      @Assisted IdentifiedUser expectedUser) {
+    super(keyCheckerFactory.create().setExpectedUser(expectedUser));
     this.repoManager = repoManager;
     this.allUsers = allUsers;
   }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java
index 725a6e1..e4c81df 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyChecker.java
@@ -19,20 +19,34 @@
 import static com.google.gerrit.extensions.common.GpgKeyInfo.Status.TRUSTED;
 import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_KEY;
+import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_REASON;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.KEY_COMPROMISED;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.KEY_RETIRED;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.KEY_SUPERSEDED;
+import static org.bouncycastle.bcpg.sig.RevocationReasonTags.NO_REASON;
+import static org.bouncycastle.openpgp.PGPSignature.DIRECT_KEY;
+import static org.bouncycastle.openpgp.PGPSignature.KEY_REVOCATION;
 
 import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
 
 import org.bouncycastle.bcpg.SignatureSubpacket;
 import org.bouncycastle.bcpg.SignatureSubpacketTags;
+import org.bouncycastle.bcpg.sig.RevocationKey;
+import org.bouncycastle.bcpg.sig.RevocationReason;
 import org.bouncycastle.openpgp.PGPException;
 import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
 import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
 import org.bouncycastle.openpgp.PGPSignature;
+import org.bouncycastle.openpgp.operator.bc.BcPGPContentVerifierBuilderProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -42,69 +56,91 @@
 
 /** Checker for GPG public keys for use in a push certificate. */
 public class PublicKeyChecker {
+  private static final Logger log =
+      LoggerFactory.getLogger(PublicKeyChecker.class);
+
   // https://tools.ietf.org/html/rfc4880#section-5.2.3.13
   private static final int COMPLETE_TRUST = 120;
 
-  private final Map<Long, Fingerprint> trusted;
-  private final int maxTrustDepth;
-
-  /** Create a new checker that does not check the web of trust. */
-  public PublicKeyChecker() {
-    this(0, null);
-  }
+  private PublicKeyStore store;
+  private Map<Long, Fingerprint> trusted;
+  private int maxTrustDepth;
+  private Date effectiveTime = new Date();
 
   /**
+   * Enable web-of-trust checks.
+   * <p>
+   * If enabled, a store must be set with {@link #setStore(PublicKeyStore)}.
+   * (These methods are separate since the store is a closeable resource that
+   * may not be available when reading trusted keys from a config.)
+   *
    * @param maxTrustDepth maximum depth to search while looking for a trusted
    *     key.
-   * @param trusted ultimately trusted key fingerprints; may not be empty. If
-   *     null, disable web-of-trust checks.
+   * @param trusted ultimately trusted key fingerprints, keyed by fingerprint;
+   *     may not be empty. To construct a map, see {@link
+   *     Fingerprint#byId(Iterable)}.
+   * @return a reference to this object.
    */
-  public PublicKeyChecker(int maxTrustDepth, Collection<Fingerprint> trusted) {
-    if (trusted != null) {
-      if (maxTrustDepth <= 0) {
+  public PublicKeyChecker enableTrust(int maxTrustDepth,
+      Map<Long, Fingerprint> trusted) {
+    if (maxTrustDepth <= 0) {
+      throw new IllegalArgumentException(
+          "maxTrustDepth must be positive, got: " + maxTrustDepth);
+    }
+    if (trusted == null || trusted.isEmpty()) {
         throw new IllegalArgumentException(
-            "maxTrustDepth must be positive, got: " + maxTrustDepth);
-      }
-      if (trusted.isEmpty()) {
-        throw new IllegalArgumentException("at least one trusted key required");
-      }
-      this.trusted = new HashMap<>();
-      for (Fingerprint fp : trusted) {
-        this.trusted.put(fp.getId(), fp);
-      }
-    } else {
-      this.trusted = null;
+            "at least one trusted key is required");
     }
     this.maxTrustDepth = maxTrustDepth;
+    this.trusted = trusted;
+    return this;
   }
 
-  /**
-   * Check a public key, including its web of trust.
-   *
-   * @param key the public key.
-   * @param store a store to read public keys from for trust checks. If this
-   *     store is not configured for web-of-trust checks, this argument is
-   *     ignored.
-   * @return the result of the check.
-   */
-  public final CheckResult check(PGPPublicKey key, PublicKeyStore store) {
-    if (trusted == null) {
-      return check(key);
-    } else if (store == null) {
-      throw new IllegalArgumentException(
-          "PublicKeyStore required for web of trust checks");
+  /** Disable web-of-trust checks. */
+  public PublicKeyChecker disableTrust() {
+    trusted = null;
+    return this;
+  }
+
+  /** Set the public key store for reading keys referenced in signatures. */
+  public PublicKeyChecker setStore(PublicKeyStore store) {
+    if (store == null) {
+      throw new IllegalArgumentException("PublicKeyStore is required");
     }
-    return check(key, store, 0, true, new HashSet<Fingerprint>());
+    this.store = store;
+    return this;
   }
 
   /**
-   * Check only a public key, not including its web of trust.
+   * Set the effective time for checking the key.
+   * <p>
+   * If set, check whether the key should be considered valid (e.g. unexpired)
+   * as of this time.
+   *
+   * @param effectiveTime effective time.
+   * @return a reference to this object.
+   */
+  public PublicKeyChecker setEffectiveTime(Date effectiveTime) {
+    this.effectiveTime = effectiveTime;
+    return this;
+  }
+
+  protected Date getEffectiveTime() {
+    return effectiveTime;
+  }
+
+  /**
+   * Check a public key.
    *
    * @param key the public key.
    * @return the result of the check.
    */
   public final CheckResult check(PGPPublicKey key) {
-    return check(key, null, 0, false, null);
+    if (store == null) {
+      throw new IllegalStateException("PublicKeyStore is required");
+    }
+    return check(key, 0, true,
+        trusted != null ? new HashSet<Fingerprint>() : null);
   }
 
   /**
@@ -115,17 +151,17 @@
    *
    * @param key the public key.
    * @param depth the depth from the initial key passed to {@link #check(
-   *     PGPPublicKey, PublicKeyStore)}: 0 if this was the initial key, up to a
-   *     maximum of {@code maxTrustDepth}.
+   *     PGPPublicKey)}: 0 if this was the initial key, up to a maximum of
+   *     {@code maxTrustDepth}.
    * @return the result of the custom check.
    */
   public CheckResult checkCustom(PGPPublicKey key, int depth) {
     return CheckResult.ok();
   }
 
-  private CheckResult check(PGPPublicKey key, PublicKeyStore store, int depth,
-      boolean expand, Set<Fingerprint> seen) {
-    CheckResult basicResult = checkBasic(key);
+  private CheckResult check(PGPPublicKey key, int depth, boolean expand,
+      Set<Fingerprint> seen) {
+    CheckResult basicResult = checkBasic(key, effectiveTime);
     CheckResult customResult = checkCustom(key, depth);
     CheckResult trustResult = checkWebOfTrust(key, store, depth, seen);
     if (!expand && !trustResult.isTrusted()) {
@@ -160,28 +196,189 @@
     return CheckResult.create(status, problems);
   }
 
-  private CheckResult checkBasic(PGPPublicKey key) {
+  private CheckResult checkBasic(PGPPublicKey key, Date now) {
     List<String> problems = new ArrayList<>(2);
-    if (key.isRevoked()) {
-      // TODO(dborowitz): isRevoked is overeager:
-      // http://www.bouncycastle.org/jira/browse/BJB-45
-      problems.add("Key is revoked");
-    }
+    gatherRevocationProblems(key, now, problems);
 
-    long validSecs = key.getValidSeconds();
-    if (validSecs != 0) {
-      long createdSecs = key.getCreationTime().getTime() / 1000;
-      long nowSecs = System.currentTimeMillis() / 1000;
-      if (nowSecs - createdSecs > validSecs) {
+    long validMs = key.getValidSeconds() * 1000;
+    if (validMs != 0) {
+      long msSinceCreation = now.getTime() - key.getCreationTime().getTime();
+      if (msSinceCreation > validMs) {
         problems.add("Key is expired");
       }
     }
     return CheckResult.create(problems);
   }
 
+  private void gatherRevocationProblems(PGPPublicKey key, Date now,
+      List<String> problems) {
+    try {
+      List<PGPSignature> revocations = new ArrayList<>();
+      Map<Long, RevocationKey> revokers = new HashMap<>();
+      PGPSignature selfRevocation =
+          scanRevocations(key, now, revocations, revokers);
+      if (selfRevocation != null) {
+        RevocationReason reason = getRevocationReason(selfRevocation);
+        if (isRevocationValid(selfRevocation, reason, now)) {
+          problems.add(reasonToString(reason));
+        }
+      } else {
+        checkRevocations(key, revocations, revokers, problems);
+      }
+    } catch (PGPException | IOException e) {
+      problems.add("Error checking key revocation");
+    }
+  }
+
+  private static boolean isRevocationValid(PGPSignature revocation,
+      RevocationReason reason, Date now) {
+    // RFC4880 states:
+    // "If a key has been revoked because of a compromise, all signatures
+    // created by that key are suspect. However, if it was merely superseded or
+    // retired, old signatures are still valid."
+    //
+    // Note that GnuPG does not implement this correctly, as it does not
+    // consider the revocation reason and timestamp when checking whether a
+    // signature (data or certification) is valid.
+    return reason.getRevocationReason() == KEY_COMPROMISED
+        || revocation.getCreationTime().before(now);
+  }
+
+  private PGPSignature scanRevocations(PGPPublicKey key, Date now,
+      List<PGPSignature> revocations, Map<Long, RevocationKey> revokers)
+      throws PGPException {
+    @SuppressWarnings("unchecked")
+    Iterator<PGPSignature> allSigs = key.getSignatures();
+    while (allSigs.hasNext()) {
+      PGPSignature sig = allSigs.next();
+      switch (sig.getSignatureType()) {
+        case KEY_REVOCATION:
+          if (sig.getKeyID() == key.getKeyID()) {
+            sig.init(new BcPGPContentVerifierBuilderProvider(), key);
+            if (sig.verifyCertification(key)) {
+              return sig;
+            }
+          } else {
+            RevocationReason reason = getRevocationReason(sig);
+            if (reason != null && isRevocationValid(sig, reason, now)) {
+              revocations.add(sig);
+            }
+          }
+          break;
+        case DIRECT_KEY:
+          RevocationKey r = getRevocationKey(key, sig);
+          if (r != null) {
+            revokers.put(Fingerprint.getId(r.getFingerprint()), r);
+          }
+          break;
+      }
+    }
+    return null;
+  }
+
+  private RevocationKey getRevocationKey(PGPPublicKey key, PGPSignature sig)
+      throws PGPException {
+    if (sig.getKeyID() != key.getKeyID()) {
+      return null;
+    }
+    SignatureSubpacket sub =
+        sig.getHashedSubPackets().getSubpacket(REVOCATION_KEY);
+    if (sub == null) {
+      return null;
+    }
+    sig.init(new BcPGPContentVerifierBuilderProvider(), key);
+    if (!sig.verifyCertification(key)) {
+      return null;
+    }
+
+    return new RevocationKey(sub.isCritical(), sub.getData());
+  }
+
+  private void checkRevocations(PGPPublicKey key,
+      List<PGPSignature> revocations, Map<Long, RevocationKey> revokers,
+      List<String> problems)
+      throws PGPException, IOException {
+    for (PGPSignature revocation : revocations) {
+      RevocationKey revoker = revokers.get(revocation.getKeyID());
+      if (revoker == null) {
+        continue; // Not a designated revoker.
+      }
+      byte[] rfp = revoker.getFingerprint();
+      PGPPublicKeyRing revokerKeyRing = store.get(rfp);
+      if (revokerKeyRing == null) {
+        // Revoker is authorized and there is a revocation signature by this
+        // revoker, but the key is not in the store so we can't verify the
+        // signature.
+        log.info("Key " + Fingerprint.toString(key.getFingerprint())
+            + " is revoked by " + Fingerprint.toString(rfp)
+            + ", which is not in the store. Assuming revocation is valid.");
+        problems.add(reasonToString(getRevocationReason(revocation)));
+        continue;
+      }
+      PGPPublicKey rk = revokerKeyRing.getPublicKey();
+      if (rk.getAlgorithm() != revoker.getAlgorithm()) {
+        continue;
+      }
+      if (!checkBasic(rk, revocation.getCreationTime()).isOk()) {
+        // Revoker's key was expired or revoked at time of revocation, so the
+        // revocation is invalid.
+        continue;
+      }
+      revocation.init(new BcPGPContentVerifierBuilderProvider(), rk);
+      if (revocation.verifyCertification(key)) {
+        problems.add(reasonToString(getRevocationReason(revocation)));
+      }
+    }
+  }
+
+  private static RevocationReason getRevocationReason(PGPSignature sig) {
+    if (sig.getSignatureType() != KEY_REVOCATION) {
+      throw new IllegalArgumentException(
+          "Expected KEY_REVOCATION signature, got " + sig.getSignatureType());
+    }
+    SignatureSubpacket sub =
+        sig.getHashedSubPackets().getSubpacket(REVOCATION_REASON);
+    if (sub == null) {
+      return null;
+    }
+    return new RevocationReason(sub.isCritical(), sub.getData());
+  }
+
+  private static String reasonToString(RevocationReason reason) {
+    StringBuilder r = new StringBuilder("Key is revoked (");
+    if (reason == null) {
+      return r.append("no reason provided)").toString();
+    }
+    switch (reason.getRevocationReason()) {
+      case NO_REASON:
+        r.append("no reason code specified");
+        break;
+      case KEY_SUPERSEDED:
+        r.append("superseded");
+        break;
+      case KEY_COMPROMISED:
+        r.append("key material has been compromised");
+        break;
+      case KEY_RETIRED:
+        r.append("retired and no longer valid");
+        break;
+      default:
+        r.append("reason code ")
+            .append(Integer.toString(reason.getRevocationReason()))
+            .append(')');
+        break;
+    }
+    r.append(')');
+    String desc = reason.getRevocationDescription();
+    if (!desc.isEmpty()) {
+      r.append(": ").append(desc);
+    }
+    return r.toString();
+  }
+
   private CheckResult checkWebOfTrust(PGPPublicKey key, PublicKeyStore store,
       int depth, Set<Fingerprint> seen) {
-    if (trusted == null || store == null) {
+    if (trusted == null) {
       // Trust checking not configured, server trusts all OK keys.
       return CheckResult.trusted();
     }
@@ -204,8 +401,13 @@
     Iterator<String> userIds = key.getUserIDs();
     while (userIds.hasNext()) {
       String userId = userIds.next();
+
+      // Don't check the timestamp of these certifications. This allows admins
+      // to correct untrusted keys by signing them with a trusted key, such that
+      // older signatures created by those keys retroactively appear valid.
       @SuppressWarnings("unchecked")
       Iterator<PGPSignature> sigs = key.getSignaturesForID(userId);
+
       while (sigs.hasNext()) {
         PGPSignature sig = sigs.next();
         // TODO(dborowitz): Handle CERTIFICATION_REVOCATION.
@@ -222,8 +424,7 @@
         }
         String subpacketProblem = checkTrustSubpacket(sig, depth);
         if (subpacketProblem == null) {
-          CheckResult signerResult =
-              check(signer, store, depth + 1, false, seen);
+          CheckResult signerResult = check(signer, depth + 1, false, seen);
           if (signerResult.isTrusted()) {
             return CheckResult.trusted();
           }
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
index b2798f5..3d939a1 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PublicKeyStore.java
@@ -83,7 +83,7 @@
    * @param sig signature object.
    * @param data signed payload.
    * @return the key chosen from {@code keyRings} that was able to verify the
-   *     signature, or null if none was found.
+   *     signature, or {@code null} if none was found.
    * @throws PGPException if an error occurred verifying the signature.
    */
   public static PGPPublicKey getSigner(Iterable<PGPPublicKeyRing> keyRings,
@@ -107,7 +107,7 @@
    * @param userId user ID being certified.
    * @param key key being certified.
    * @return the key chosen from {@code keyRings} that was able to verify the
-   *     certification, or null if none was found.
+   *     certification, or {@code null} if none was found.
    * @throws PGPException if an error occurred verifying the certification.
    */
   public static PGPPublicKey getSigner(Iterable<PGPPublicKeyRing> keyRings,
@@ -178,15 +178,39 @@
    */
   public PGPPublicKeyRingCollection get(long keyId)
       throws PGPException, IOException {
+    return new PGPPublicKeyRingCollection(get(keyId, null));
+  }
+
+  /**
+   * Read public key with the given fingerprint.
+   * <p>
+   * Keys should not be trusted unless checked with {@link PublicKeyChecker}.
+   * <p>
+   * Multiple calls to this method use the same state of the key ref; to reread
+   * the ref, call {@link #close()} first.
+   *
+   * @param fingerprint key fingerprint.
+   * @return the key if found, or {@code null}.
+   * @throws PGPException if an error occurred parsing the key data.
+   * @throws IOException if an error occurred reading the repository data.
+   */
+  public PGPPublicKeyRing get(byte[] fingerprint)
+      throws PGPException, IOException {
+    List<PGPPublicKeyRing> keyRings =
+        get(Fingerprint.getId(fingerprint), fingerprint);
+    return !keyRings.isEmpty() ? keyRings.get(0) : null;
+  }
+
+  private List<PGPPublicKeyRing> get(long keyId, byte[] fp) throws IOException {
     if (reader == null) {
       load();
     }
     if (notes == null) {
-      return empty();
+      return Collections.emptyList();
     }
     Note note = notes.getNote(keyObjectId(keyId));
     if (note == null) {
-      return empty();
+      return Collections.emptyList();
     }
 
     List<PGPPublicKeyRing> keys = new ArrayList<>();
@@ -200,12 +224,16 @@
         }
         Object obj = it.next();
         if (obj instanceof PGPPublicKeyRing) {
-          keys.add((PGPPublicKeyRing) obj);
+          PGPPublicKeyRing kr = (PGPPublicKeyRing) obj;
+          if (fp == null
+              || Arrays.equals(fp, kr.getPublicKey().getFingerprint())) {
+            keys.add(kr);
+          }
         }
         checkState(!it.hasNext(),
             "expected one PGP object per ArmoredInputStream");
       }
-      return new PGPPublicKeyRingCollection(keys);
+      return keys;
     }
   }
 
@@ -375,12 +403,6 @@
     return out.toByteArray();
   }
 
-  private static PGPPublicKeyRingCollection empty()
-      throws PGPException, IOException {
-    return new PGPPublicKeyRingCollection(
-        Collections.<PGPPublicKeyRing> emptyList());
-  }
-
   public static String keyToString(PGPPublicKey key) {
     @SuppressWarnings("unchecked")
     Iterator<String> it = key.getUserIDs();
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PushCertificateChecker.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PushCertificateChecker.java
index 71068b8..0a0fff7 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PushCertificateChecker.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/PushCertificateChecker.java
@@ -67,12 +67,18 @@
   }
 
   private final PublicKeyChecker publicKeyChecker;
-  private final boolean checkNonce;
 
-  protected PushCertificateChecker(PublicKeyChecker publicKeyChecker,
-      boolean checkNonce) {
+  private boolean checkNonce;
+
+  protected PushCertificateChecker(PublicKeyChecker publicKeyChecker) {
     this.publicKeyChecker = publicKeyChecker;
+    checkNonce = true;
+  }
+
+  /** Set whether to check the status of the nonce; defaults to true. */
+  public PushCertificateChecker setCheckNonce(boolean checkNonce) {
     this.checkNonce = checkNonce;
+    return this;
   }
 
   /**
@@ -202,7 +208,10 @@
           CheckResult.bad("Signature by " + keyIdToString(sig.getKeyID())
               + " is not valid"));
     }
-    CheckResult result = publicKeyChecker.check(signer, store);
+    CheckResult result = publicKeyChecker
+        .setStore(store)
+        .setEffectiveTime(sig.getCreationTime())
+        .check(signer);
     if (!result.getProblems().isEmpty()) {
       StringBuilder err = new StringBuilder("Invalid public key ")
           .append(keyToString(signer))
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushModule.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushModule.java
index c694cb9..bc027cd 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushModule.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushModule.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.gpg;
 
 import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.EnableSignedPush;
@@ -33,6 +32,7 @@
 
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PreReceiveHook;
 import org.eclipse.jgit.transport.PreReceiveHookChain;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.SignedPushConfig;
@@ -42,6 +42,8 @@
 import java.io.IOException;
 import java.security.NoSuchAlgorithmException;
 import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Random;
 
 class SignedPushModule extends AbstractModule {
@@ -92,15 +94,22 @@
       if (!ps.isEnableSignedPush()) {
         rp.setSignedPushConfig(null);
         return;
-      }
-      if (signedPushConfig == null) {
+      } else if (signedPushConfig == null) {
         log.error("receive.enableSignedPush is true for project {} but"
             + " false in gerrit.config, so signed push verification is"
             + " disabled", project.get());
+        rp.setSignedPushConfig(null);
+        return;
       }
       rp.setSignedPushConfig(signedPushConfig);
-      rp.setPreReceiveHook(PreReceiveHookChain.newChain(Lists.newArrayList(
-          hook, rp.getPreReceiveHook())));
+
+      List<PreReceiveHook> hooks = new ArrayList<>(3);
+      if (ps.isRequireSignedPush()) {
+        hooks.add(SignedPushPreReceiveHook.Required.INSTANCE);
+      }
+      hooks.add(hook);
+      hooks.add(rp.getPreReceiveHook());
+      rp.setPreReceiveHook(PreReceiveHookChain.newChain(hooks));
     }
   }
 
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
index 875c77c..cdc3c62 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/SignedPushPreReceiveHook.java
@@ -36,6 +36,21 @@
  */
 @Singleton
 public class SignedPushPreReceiveHook implements PreReceiveHook {
+  public static class Required implements PreReceiveHook {
+    public static final Required INSTANCE = new Required();
+
+    @Override
+    public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
+      if (rp.getPushCertificate() == null) {
+        rp.sendMessage("ERROR: Signed push is required");
+        reject(commands, "push cert error");
+      }
+    }
+
+    private Required() {
+    }
+  }
+
   private final Provider<IdentifiedUser> user;
   private final GerritPushCertificateChecker.Factory checkerFactory;
 
@@ -54,7 +69,8 @@
     if (cert == null) {
       return;
     }
-    CheckResult result = checkerFactory.create(user.get(), true)
+    CheckResult result = checkerFactory.create(user.get())
+        .setCheckNonce(true)
         .check(cert)
         .getCheckResult();
     if (!isAllowed(result, commands)) {
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
index 3821135..e6720db 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/api/GpgApiAdapterImpl.java
@@ -95,8 +95,10 @@
       IdentifiedUser expectedUser) throws GpgException {
     try {
       PushCertificate cert = PushCertificateParser.fromString(certStr);
-      PushCertificateChecker.Result result =
-          pushCertCheckerFactory.create(expectedUser, false).check(cert);
+      PushCertificateChecker.Result result = pushCertCheckerFactory
+          .create(expectedUser)
+          .setCheckNonce(false)
+          .check(cert);
       PushCertificateInfo info = new PushCertificateInfo();
       info.certificate = certStr;
       info.key = GpgKeys.toJson(result.getPublicKey(), result.getCheckResult());
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 900bcaf..a136007 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
@@ -163,7 +163,7 @@
               found = true;
               GpgKeyInfo info = toJson(
                   keyRing.getPublicKey(),
-                  checkerFactory.create(rsrc.getUser()),
+                  checkerFactory.create(rsrc.getUser(), store),
                   store);
               keys.put(info.id, info);
               info.id = null;
@@ -197,7 +197,7 @@
       try (PublicKeyStore store = storeProvider.get()) {
         return toJson(
             rsrc.getKeyRing().getPublicKey(),
-            checkerFactory.create(rsrc.getUser()),
+            checkerFactory.create().setExpectedUser(rsrc.getUser()),
             store);
       }
     }
@@ -261,7 +261,7 @@
 
   static GpgKeyInfo toJson(PGPPublicKey key, PublicKeyChecker checker,
       PublicKeyStore store) throws IOException {
-    return toJson(key, checker.check(key, store));
+    return toJson(key, checker.setStore(store).check(key));
   }
 
   public static void toJson(GpgKeyInfo info, CheckResult checkResult) {
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 9b18ea2..91c4494 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
@@ -36,6 +36,7 @@
 import com.google.gerrit.gpg.CheckResult;
 import com.google.gerrit.gpg.Fingerprint;
 import com.google.gerrit.gpg.GerritPublicKeyChecker;
+import com.google.gerrit.gpg.PublicKeyChecker;
 import com.google.gerrit.gpg.PublicKeyStore;
 import com.google.gerrit.gpg.server.PostGpgKeys.Input;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -193,7 +194,9 @@
       for (PGPPublicKeyRing keyRing : keyRings) {
         PGPPublicKey key = keyRing.getPublicKey();
         // Don't check web of trust; admins can fill in certifications later.
-        CheckResult result = checkerFactory.create(rsrc.getUser()).check(key);
+        CheckResult result = checkerFactory.create(rsrc.getUser(), store)
+            .disableTrust()
+            .check(key);
         if (!result.isOk()) {
           throw new BadRequestException(String.format(
               "Problems with public key %s:\n%s",
@@ -245,12 +248,12 @@
       throws IOException {
     // Unlike when storing keys, include web-of-trust checks when producing
     // result JSON, so the user at least knows of any issues.
-    GerritPublicKeyChecker checker = checkerFactory.create(user);
+    PublicKeyChecker checker = checkerFactory.create(user, store);
     Map<String, GpgKeyInfo> infos =
         Maps.newHashMapWithExpectedSize(keys.size() + deleted.size());
     for (PGPPublicKeyRing keyRing : keys) {
       PGPPublicKey key = keyRing.getPublicKey();
-      CheckResult result = checker.check(key, store);
+      CheckResult result = checker.check(key);
       GpgKeyInfo info = GpgKeys.toJson(key, result);
       infos.put(info.id, info);
       info.id = null;
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 05617ff..4df9d37 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
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.gpg;
 
-import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.gpg.GerritPublicKeyChecker.toExtIdKey;
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithSecondUserId;
 import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyA;
 import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyB;
 import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyC;
@@ -32,7 +32,6 @@
 import com.google.common.collect.Iterators;
 import com.google.gerrit.extensions.common.GpgKeyInfo.Status;
 import com.google.gerrit.gpg.testutil.TestKey;
-import com.google.gerrit.gpg.testutil.TestKeys;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -67,6 +66,7 @@
 import org.junit.Test;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
@@ -171,8 +171,9 @@
 
   @Test
   public void defaultGpgCertificationMatchesEmail() throws Exception {
-    TestKey key = TestKeys.key5();
-    GerritPublicKeyChecker checker = checkerFactory.create(user);
+    TestKey key = validKeyWithSecondUserId();
+    PublicKeyChecker checker = checkerFactory.create(user, store)
+        .disableTrust();
     assertProblems(
         checker.check(key.getPublicKey()), Status.BAD,
         "Key must contain a valid certification for one of the following "
@@ -181,16 +182,18 @@
           + "  username:user");
 
     addExternalId("test", "test", "test5@example.com");
-    checker = checkerFactory.create(user);
+    checker = checkerFactory.create(user, store)
+        .disableTrust();
     assertNoProblems(checker.check(key.getPublicKey()));
   }
 
   @Test
   public void defaultGpgCertificationDoesNotMatchEmail() throws Exception {
     addExternalId("test", "test", "nobody@example.com");
-    GerritPublicKeyChecker checker = checkerFactory.create(user);
+    PublicKeyChecker checker = checkerFactory.create(user, store)
+        .disableTrust();
     assertProblems(
-        checker.check(TestKeys.key5().getPublicKey()), Status.BAD,
+        checker.check(validKeyWithSecondUserId().getPublicKey()), Status.BAD,
         "Key must contain a valid certification for one of the following "
           + "identities:\n"
           + "  gerrit:user\n"
@@ -202,16 +205,18 @@
   @Test
   public void manualCertificationMatchesExternalId() throws Exception {
     addExternalId("foo", "myId", null);
-    GerritPublicKeyChecker checker = checkerFactory.create(user);
-    assertNoProblems(checker.check(TestKeys.key5().getPublicKey()));
+    PublicKeyChecker checker = checkerFactory.create(user, store)
+        .disableTrust();
+    assertNoProblems(checker.check(validKeyWithSecondUserId().getPublicKey()));
   }
 
   @Test
   public void manualCertificationDoesNotMatchExternalId() throws Exception {
     addExternalId("foo", "otherId", null);
-    GerritPublicKeyChecker checker = checkerFactory.create(user);
+    PublicKeyChecker checker = checkerFactory.create(user, store)
+        .disableTrust();
     assertProblems(
-        checker.check(TestKeys.key5().getPublicKey()), Status.BAD,
+        checker.check(validKeyWithSecondUserId().getPublicKey()), Status.BAD,
         "Key must contain a valid certification for one of the following "
           + "identities:\n"
           + "  foo:otherId\n"
@@ -225,14 +230,17 @@
         db.accountExternalIds().byAccount(user.getAccountId()));
     reloadUser();
 
-    TestKey key = TestKeys.key5();
-    GerritPublicKeyChecker checker = checkerFactory.create(user);
+    TestKey key = validKeyWithSecondUserId();
+    PublicKeyChecker checker = checkerFactory.create(user, store)
+        .disableTrust();
     assertProblems(
         checker.check(key.getPublicKey()), Status.BAD,
         "No identities found for user; check"
           + " http://test/#/settings/web-identities");
 
-    checker = checkerFactory.create();
+    checker = checkerFactory.create()
+        .setStore(store)
+        .disableTrust();
     assertProblems(
         checker.check(key.getPublicKey()), Status.BAD,
         "Key is not associated with any users");
@@ -264,14 +272,14 @@
     add(keyE(), addUser("userE"));
 
     // Checker for A, checking A.
-    GerritPublicKeyChecker checkerA = checkerFactory.create(user);
-    assertNoProblems(checkerA.check(keyA.getPublicKey(), store));
+    PublicKeyChecker checkerA = checkerFactory.create(user, store);
+    assertNoProblems(checkerA.check(keyA.getPublicKey()));
 
     // Checker for B, checking B. Trust chain and IDs are correct, so the only
     // problem is with the key itself.
-    GerritPublicKeyChecker checkerB = checkerFactory.create(userB);
+    PublicKeyChecker checkerB = checkerFactory.create(userB, store);
     assertProblems(
-        checkerB.check(keyB.getPublicKey(), store), Status.BAD,
+        checkerB.check(keyB.getPublicKey()), Status.BAD,
         "Key is expired");
   }
 
@@ -294,9 +302,9 @@
     add(keyE(), addUser("userE"));
 
     // Checker for A, checking B.
-    GerritPublicKeyChecker checkerA = checkerFactory.create(user);
+    PublicKeyChecker checkerA = checkerFactory.create(user, store);
     assertProblems(
-        checkerA.check(keyB.getPublicKey(), store), Status.BAD,
+        checkerA.check(keyB.getPublicKey()), Status.BAD,
         "Key is expired",
         "Key must contain a valid certification for one of the following"
             + " identities:\n"
@@ -306,9 +314,9 @@
             + "  username:user");
 
     // Checker for B, checking A.
-    GerritPublicKeyChecker checkerB = checkerFactory.create(userB);
+    PublicKeyChecker checkerB = checkerFactory.create(userB, store);
     assertProblems(
-        checkerB.check(keyA.getPublicKey(), store), Status.BAD,
+        checkerB.check(keyA.getPublicKey()), Status.BAD,
         "Key must contain a valid certification for one of the following"
             + " identities:\n"
             + "  gerrit:userB\n"
@@ -325,9 +333,9 @@
     TestKey keyA = add(keyA(), user);
     TestKey keyB = add(keyB(), addUser("userB"));
 
-    GerritPublicKeyChecker checker = checkerFactory.create(user);
+    PublicKeyChecker checker = checkerFactory.create(user, store);
     assertProblems(
-        checker.check(keyA.getPublicKey(), store), Status.OK,
+        checker.check(keyA.getPublicKey()), Status.OK,
         "No path to a trusted key",
         "Certification by " + keyToString(keyB.getPublicKey())
             + " is valid, but key is not trusted",
@@ -352,15 +360,16 @@
 
     // This checker can check any key, so the only problems come from issues
     // with the keys themselves, not having invalid user IDs.
-    GerritPublicKeyChecker checker = checkerFactory.create();
-    assertNoProblems(checker.check(keyA.getPublicKey(), store));
+    PublicKeyChecker checker = checkerFactory.create()
+        .setStore(store);
+    assertNoProblems(checker.check(keyA.getPublicKey()));
     assertProblems(
-        checker.check(keyB.getPublicKey(), store), Status.BAD,
+        checker.check(keyB.getPublicKey()), Status.BAD,
         "Key is expired");
-    assertNoProblems(checker.check(keyC.getPublicKey(), store));
-    assertNoProblems(checker.check(keyD.getPublicKey(), store));
+    assertNoProblems(checker.check(keyC.getPublicKey()));
+    assertNoProblems(checker.check(keyD.getPublicKey()));
     assertProblems(
-        checker.check(keyE.getPublicKey(), store), Status.BAD,
+        checker.check(keyE.getPublicKey()), Status.BAD,
         "Key is expired",
         "No path to a trusted key");
   }
@@ -382,8 +391,8 @@
     keyRingB = PGPPublicKeyRing.insertPublicKey(keyRingB, keyB);
     add(keyRingB, addUser("userB"));
 
-    GerritPublicKeyChecker checkerA = checkerFactory.create(user);
-    assertProblems(checkerA.check(keyA.getPublicKey(), store), Status.OK,
+    PublicKeyChecker checkerA = checkerFactory.create(user, store);
+    assertProblems(checkerA.check(keyA.getPublicKey()), Status.OK,
         "No path to a trusted key",
         "Certification by " + keyToString(keyB)
             + " is valid, but key is not trusted",
@@ -424,11 +433,13 @@
   }
 
   private void assertProblems(CheckResult result, Status expectedStatus,
-      String... expectedProblems) throws Exception {
-    checkArgument(expectedProblems.length > 0);
+      String first, String... rest) throws Exception {
+    List<String> expectedProblems = new ArrayList<>();
+    expectedProblems.add(first);
+    expectedProblems.addAll(Arrays.asList(rest));
     assertThat(result.getStatus()).isEqualTo(expectedStatus);
     assertThat(result.getProblems())
-        .containsExactly((Object[]) expectedProblems)
+        .containsExactlyElementsIn(expectedProblems)
         .inOrder();
   }
 
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
index d1f5731..742bf1a 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyCheckerTest.java
@@ -15,6 +15,14 @@
 package com.google.gerrit.gpg;
 
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static com.google.gerrit.gpg.testutil.TestKeys.expiredKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.keyRevokedByExpiredKeyAfterExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.keyRevokedByExpiredKeyBeforeExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.revokedCompromisedKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.revokedNoLongerUsedKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.selfRevokedKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
 import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyA;
 import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyB;
 import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyC;
@@ -25,11 +33,15 @@
 import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyH;
 import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyI;
 import static com.google.gerrit.gpg.testutil.TestTrustKeys.keyJ;
+import static org.bouncycastle.bcpg.SignatureSubpacketTags.REVOCATION_KEY;
+import static org.bouncycastle.openpgp.PGPSignature.DIRECT_KEY;
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.gpg.testutil.TestKey;
-import com.google.gerrit.gpg.testutil.TestKeys;
 
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPSignature;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -41,9 +53,15 @@
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 
 public class PublicKeyCheckerTest {
   @Rule
@@ -72,22 +90,33 @@
 
   @Test
   public void validKey() throws Exception {
-    assertProblems(TestKeys.key1());
+    assertNoProblems(validKeyWithoutExpiration());
   }
 
   @Test
   public void keyExpiringInFuture() throws Exception {
-    assertProblems(TestKeys.key2());
+    TestKey k = validKeyWithExpiration();
+
+    PublicKeyChecker checker = new PublicKeyChecker()
+        .setStore(store);
+    assertNoProblems(checker, k);
+
+    checker.setEffectiveTime(parseDate("2015-07-10 12:00:00 -0400"));
+    assertNoProblems(checker, k);
+
+    checker.setEffectiveTime(parseDate("2075-07-10 12:00:00 -0400"));
+    assertProblems(checker, k, "Key is expired");
   }
 
   @Test
-  public void expiredKey() throws Exception {
-    assertProblems(TestKeys.key3(), "Key is expired");
+  public void expiredKeyIsExpired() throws Exception {
+    assertProblems(expiredKey(), "Key is expired");
   }
 
   @Test
-  public void selfRevokedKey() throws Exception {
-    assertProblems(TestKeys.key4(), "Key is revoked");
+  public void selfRevokedKeyIsRevoked() throws Exception {
+    assertProblems(selfRevokedKey(),
+        "Key is revoked (key material has been compromised)");
   }
 
   // Test keys specific to this test are at the bottom of this class. Each test
@@ -112,10 +141,10 @@
     save();
 
     PublicKeyChecker checker = newChecker(2, kb, kd);
-    assertProblems(checker, ka);
+    assertNoProblems(checker, ka);
     assertProblems(checker, kb, "Key is expired");
-    assertProblems(checker, kc);
-    assertProblems(checker, kd);
+    assertNoProblems(checker, kc);
+    assertNoProblems(checker, kd);
     assertProblems(checker, ke, "Key is expired", "No path to a trusted key");
   }
 
@@ -166,17 +195,132 @@
 
     // J trusts I to a depth of 1, so I itself is valid, but I's certification
     // of K is not valid.
-    assertProblems(checker, ki);
+    assertNoProblems(checker, ki);
     assertProblems(checker, kh,
         "No path to a trusted key", notTrusted(ki));
   }
 
-  private PublicKeyChecker newChecker(int maxTrustDepth, TestKey... trusted) {
-    List<Fingerprint> fps = new ArrayList<>(trusted.length);
-    for (TestKey k : trusted) {
-      fps.add(new Fingerprint(k.getPublicKey().getFingerprint()));
+  @Test
+  public void revokedKeyDueToCompromise() throws Exception {
+    TestKey k = add(revokedCompromisedKey());
+    add(validKeyWithoutExpiration());
+    save();
+
+    assertProblems(k,
+        "Key is revoked (key material has been compromised):"
+          + " test6 compromised");
+
+    PGPPublicKeyRing kr = removeRevokers(k.getPublicKeyRing());
+    store.add(kr);
+    save();
+
+    // Key no longer specified as revoker.
+    assertNoProblems(kr.getPublicKey());
+  }
+
+  @Test
+  public void revokedKeyDueToCompromiseRevokesKeyRetroactively()
+      throws Exception {
+    TestKey k = add(revokedCompromisedKey());
+    add(validKeyWithoutExpiration());
+    save();
+
+    String problem =
+        "Key is revoked (key material has been compromised): test6 compromised";
+    assertProblems(k, problem);
+
+    SimpleDateFormat df = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss");
+    PublicKeyChecker checker = new PublicKeyChecker()
+        .setStore(store)
+        .setEffectiveTime(df.parse("2010-01-01 12:00:00"));
+    assertProblems(checker, k, problem);
+  }
+
+  @Test
+  public void revokedByKeyNotPresentInStore() throws Exception {
+    TestKey k = add(revokedCompromisedKey());
+    save();
+
+    assertProblems(k,
+        "Key is revoked (key material has been compromised):"
+          + " test6 compromised");
+  }
+
+  @Test
+  public void revokedKeyDueToNoLongerBeingUsed() throws Exception {
+    TestKey k = add(revokedNoLongerUsedKey());
+    add(validKeyWithoutExpiration());
+    save();
+
+    assertProblems(k,
+        "Key is revoked (retired and no longer valid): test7 not used");
+  }
+
+  @Test
+  public void revokedKeyDueToNoLongerBeingUsedDoesNotRevokeKeyRetroactively()
+      throws Exception {
+    TestKey k = add(revokedNoLongerUsedKey());
+    add(validKeyWithoutExpiration());
+    save();
+
+    assertProblems(k,
+        "Key is revoked (retired and no longer valid): test7 not used");
+
+    PublicKeyChecker checker = new PublicKeyChecker()
+        .setStore(store)
+        .setEffectiveTime(parseDate("2010-01-01 12:00:00 -0400"));
+    assertNoProblems(checker, k);
+  }
+
+  @Test
+  public void keyRevokedByExpiredKeyAfterExpirationIsNotRevoked()
+      throws Exception {
+    TestKey k = add(keyRevokedByExpiredKeyAfterExpiration());
+    add(expiredKey());
+    save();
+
+    PublicKeyChecker checker = new PublicKeyChecker().setStore(store);
+    assertNoProblems(checker, k);
+  }
+
+  @Test
+  public void keyRevokedByExpiredKeyBeforeExpirationIsRevoked()
+      throws Exception {
+    TestKey k = add(keyRevokedByExpiredKeyBeforeExpiration());
+    add(expiredKey());
+    save();
+
+    PublicKeyChecker checker = new PublicKeyChecker().setStore(store);
+    assertProblems(checker, k,
+        "Key is revoked (retired and no longer valid): test9 not used");
+
+    // Set time between key creation and revocation.
+    checker.setEffectiveTime(parseDate("2005-08-01 13:00:00 -0400"));
+    assertNoProblems(checker, k);
+  }
+
+  private PGPPublicKeyRing removeRevokers(PGPPublicKeyRing kr) {
+    PGPPublicKey k = kr.getPublicKey();
+    @SuppressWarnings("unchecked")
+    Iterator<PGPSignature> sigs = k.getSignaturesOfType(DIRECT_KEY);
+    while (sigs.hasNext()) {
+      PGPSignature sig = sigs.next();
+      if (sig.getHashedSubPackets().hasSubpacket(REVOCATION_KEY)) {
+        k = PGPPublicKey.removeCertification(k, sig);
+      }
     }
-    return new PublicKeyChecker(maxTrustDepth, fps);
+    return PGPPublicKeyRing.insertPublicKey(kr, k);
+  }
+
+  private PublicKeyChecker newChecker(int maxTrustDepth, TestKey... trusted) {
+    Map<Long, Fingerprint> fps = new HashMap<>();
+    for (TestKey k : trusted) {
+      Fingerprint fp = new Fingerprint(k.getPublicKey().getFingerprint());
+      fps.put(fp.getId(), fp);
+    }
+    return new PublicKeyChecker()
+        .enableTrust(maxTrustDepth, fps)
+        .setStore(store);
   }
 
   private TestKey add(TestKey k) {
@@ -201,18 +345,53 @@
   }
 
   private void assertProblems(PublicKeyChecker checker, TestKey k,
-      String... expected) {
-    CheckResult result = checker.check(k.getPublicKey(), store);
-    assertEquals(Arrays.asList(expected), result.getProblems());
+      String first, String... rest) {
+    CheckResult result = checker.setStore(store)
+        .check(k.getPublicKey());
+    assertEquals(list(first, rest), result.getProblems());
   }
 
-  private void assertProblems(TestKey tk, String... expected) throws Exception {
-    CheckResult result = new PublicKeyChecker().check(tk.getPublicKey(), store);
-    assertEquals(Arrays.asList(expected), result.getProblems());
+  private void assertNoProblems(PublicKeyChecker checker, TestKey k) {
+    CheckResult result = checker.setStore(store)
+        .check(k.getPublicKey());
+    assertEquals(Collections.emptyList(), result.getProblems());
+  }
+
+  private void assertProblems(TestKey tk, String first, String... rest) {
+    assertProblems(tk.getPublicKey(), first, rest);
+  }
+
+  private void assertNoProblems(TestKey tk) {
+    assertNoProblems(tk.getPublicKey());
+  }
+
+  private void assertProblems(PGPPublicKey k, String first, String... rest) {
+    CheckResult result = new PublicKeyChecker()
+        .setStore(store)
+        .check(k);
+    assertEquals(list(first, rest), result.getProblems());
+  }
+
+  private void assertNoProblems(PGPPublicKey k) {
+    CheckResult result = new PublicKeyChecker()
+        .setStore(store)
+        .check(k);
+    assertEquals(Collections.emptyList(), result.getProblems());
   }
 
   private static String notTrusted(TestKey k) {
     return "Certification by " + keyToString(k.getPublicKey())
         + " is valid, but key is not trusted";
   }
+
+  private static Date parseDate(String str) throws Exception {
+    return new SimpleDateFormat("YYYY-MM-dd HH:mm:ss Z").parse(str);
+  }
+
+  private static List<String> list(String first, String[] rest) {
+    List<String> all = new ArrayList<>();
+    all.add(first);
+    all.addAll(Arrays.asList(rest));
+    return all;
+  }
 }
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java
index b1e6404..9c0a908 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PublicKeyStoreTest.java
@@ -18,13 +18,15 @@
 import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
 import static com.google.gerrit.gpg.PublicKeyStore.keyObjectId;
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithSecondUserId;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import com.google.gerrit.gpg.testutil.TestKey;
-import com.google.gerrit.gpg.testutil.TestKeys;
 
 import org.bouncycastle.openpgp.PGPPublicKey;
 import org.bouncycastle.openpgp.PGPPublicKeyRing;
@@ -61,13 +63,13 @@
 
   @Test
   public void testKeyIdToString() throws Exception {
-    PGPPublicKey key = TestKeys.key1().getPublicKey();
+    PGPPublicKey key = validKeyWithoutExpiration().getPublicKey();
     assertEquals("46328A8C", keyIdToString(key.getKeyID()));
   }
 
   @Test
   public void testKeyToString() throws Exception {
-    PGPPublicKey key = TestKeys.key1().getPublicKey();
+    PGPPublicKey key = validKeyWithoutExpiration().getPublicKey();
     assertEquals("46328A8C Testuser One <test1@example.com>"
           + " (04AE A7ED 2F82 1133 E5B1  28D1 ED06 25DC 4632 8A8C)",
         keyToString(key));
@@ -75,7 +77,7 @@
 
   @Test
   public void testKeyObjectId() throws Exception {
-    PGPPublicKey key = TestKeys.key1().getPublicKey();
+    PGPPublicKey key = validKeyWithoutExpiration().getPublicKey();
     String objId = keyObjectId(key.getKeyID()).name();
     assertEquals("ed0625dc46328a8c000000000000000000000000", objId);
     assertEquals(keyIdToString(key.getKeyID()).toLowerCase(),
@@ -84,13 +86,13 @@
 
   @Test
   public void testGet() throws Exception {
-    TestKey key1 = TestKeys.key1();
+    TestKey key1 = validKeyWithoutExpiration();
     tr.branch(REFS_GPG_KEYS)
         .commit()
         .add(keyObjectId(key1.getKeyId()).name(),
           key1.getPublicKeyArmored())
         .create();
-    TestKey key2 = TestKeys.key2();
+    TestKey key2 = validKeyWithExpiration();
     tr.branch(REFS_GPG_KEYS)
         .commit()
         .add(keyObjectId(key2.getKeyId()).name(),
@@ -103,8 +105,8 @@
 
   @Test
   public void testGetMultiple() throws Exception {
-    TestKey key1 = TestKeys.key1();
-    TestKey key2 = TestKeys.key2();
+    TestKey key1 = validKeyWithoutExpiration();
+    TestKey key2 = validKeyWithExpiration();
     tr.branch(REFS_GPG_KEYS)
         .commit()
         .add(keyObjectId(key1.getKeyId()).name(),
@@ -117,8 +119,8 @@
 
   @Test
   public void save() throws Exception {
-    TestKey key1 = TestKeys.key1();
-    TestKey key2 = TestKeys.key2();
+    TestKey key1 = validKeyWithoutExpiration();
+    TestKey key2 = validKeyWithExpiration();
     store.add(key1.getPublicKeyRing());
     store.add(key2.getPublicKeyRing());
 
@@ -130,8 +132,8 @@
 
   @Test
   public void saveAppendsToExistingList() throws Exception {
-    TestKey key1 = TestKeys.key1();
-    TestKey key2 = TestKeys.key2();
+    TestKey key1 = validKeyWithoutExpiration();
+    TestKey key2 = validKeyWithExpiration();
     tr.branch(REFS_GPG_KEYS)
         .commit()
         // Mismatched for this key ID, but we can still read it out.
@@ -161,7 +163,7 @@
 
   @Test
   public void updateExisting() throws Exception {
-    TestKey key5 = TestKeys.key5();
+    TestKey key5 = validKeyWithSecondUserId();
     PGPPublicKeyRing keyRing = key5.getPublicKeyRing();
     PGPPublicKey key = keyRing.getPublicKey();
     store.add(keyRing);
@@ -185,7 +187,7 @@
 
   @Test
   public void remove() throws Exception {
-    TestKey key1 = TestKeys.key1();
+    TestKey key1 = validKeyWithoutExpiration();
     store.add(key1.getPublicKeyRing());
     assertEquals(RefUpdate.Result.NEW, store.save(newCommitBuilder()));
     assertKeys(key1.getKeyId(), key1);
@@ -197,11 +199,11 @@
 
   @Test
   public void removeNonexisting() throws Exception {
-    TestKey key1 = TestKeys.key1();
+    TestKey key1 = validKeyWithoutExpiration();
     store.add(key1.getPublicKeyRing());
     assertEquals(RefUpdate.Result.NEW, store.save(newCommitBuilder()));
 
-    TestKey key2 = TestKeys.key2();
+    TestKey key2 = validKeyWithExpiration();
     store.remove(key2.getPublicKey().getFingerprint());
     assertEquals(RefUpdate.Result.NO_CHANGE, store.save(newCommitBuilder()));
     assertKeys(key1.getKeyId(), key1);
@@ -209,7 +211,7 @@
 
   @Test
   public void addThenRemove() throws Exception {
-    TestKey key1 = TestKeys.key1();
+    TestKey key1 = validKeyWithoutExpiration();
     store.add(key1.getPublicKeyRing());
     store.remove(key1.getPublicKey().getFingerprint());
     assertEquals(RefUpdate.Result.NO_CHANGE, store.save(newCommitBuilder()));
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PushCertificateCheckerTest.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PushCertificateCheckerTest.java
index 9bee0ef..ee07d55 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PushCertificateCheckerTest.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/PushCertificateCheckerTest.java
@@ -14,24 +14,28 @@
 
 package com.google.gerrit.gpg;
 
-import static com.google.gerrit.gpg.PublicKeyStore.REFS_GPG_KEYS;
 import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
 import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
+import static com.google.gerrit.gpg.testutil.TestKeys.expiredKey;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithExpiration;
+import static com.google.gerrit.gpg.testutil.TestKeys.validKeyWithoutExpiration;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertEquals;
 
 import com.google.gerrit.gpg.testutil.TestKey;
-import com.google.gerrit.gpg.testutil.TestKeys;
 
 import org.bouncycastle.bcpg.ArmoredOutputStream;
 import org.bouncycastle.bcpg.BCPGOutputStream;
 import org.bouncycastle.openpgp.PGPSignature;
 import org.bouncycastle.openpgp.PGPSignatureGenerator;
+import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
 import org.bouncycastle.openpgp.PGPUtil;
 import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder;
 import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
-import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.transport.PushCertificate;
 import org.eclipse.jgit.transport.PushCertificateIdent;
@@ -44,25 +48,34 @@
 import java.io.ByteArrayOutputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
 
 public class PushCertificateCheckerTest {
-  private TestRepository<?> tr;
+  private InMemoryRepository repo;
+  private PublicKeyStore store;
   private SignedPushConfig signedPushConfig;
   private PushCertificateChecker checker;
 
   @Before
   public void setUp() throws Exception {
-    TestKey key1 = TestKeys.key1();
-    TestKey key3 = TestKeys.key3();
-    tr = new TestRepository<>(new InMemoryRepository(
-        new DfsRepositoryDescription("repo")));
-    tr.branch(REFS_GPG_KEYS).commit()
-        .add(PublicKeyStore.keyObjectId(key1.getPublicKey().getKeyID()).name(),
-            key1.getPublicKeyArmored())
-        .add(PublicKeyStore.keyObjectId(key3.getPublicKey().getKeyID()).name(),
-            key3.getPublicKeyArmored())
-        .create();
+    TestKey key1 = validKeyWithoutExpiration();
+    TestKey key3 = expiredKey();
+    repo = new InMemoryRepository(new DfsRepositoryDescription("repo"));
+    store = new PublicKeyStore(repo);
+    store.add(key1.getPublicKeyRing());
+    store.add(key3.getPublicKeyRing());
+
+    PersonIdent ident = new PersonIdent("A U Thor", "author@example.com");
+    CommitBuilder cb = new CommitBuilder();
+    cb.setAuthor(ident);
+    cb.setCommitter(ident);
+    assertEquals(RefUpdate.Result.NEW, store.save(cb));
+
     signedPushConfig = new SignedPushConfig();
     signedPushConfig.setCertNonceSeed("sekret");
     signedPushConfig.setCertNonceSlopLimit(60 * 24);
@@ -70,41 +83,45 @@
   }
 
   private PushCertificateChecker newChecker(boolean checkNonce) {
-    return new PushCertificateChecker(new PublicKeyChecker(), checkNonce) {
+    PublicKeyChecker keyChecker = new PublicKeyChecker().setStore(store);
+    return new PushCertificateChecker(keyChecker) {
       @Override
       protected Repository getRepository() {
-        return tr.getRepository();
+        return repo;
       }
 
       @Override
       protected boolean shouldClose(Repository repo) {
         return false;
       }
-    };
+    }.setCheckNonce(checkNonce);
   }
 
   @Test
   public void validCert() throws Exception {
-    PushCertificate cert = newSignedCert(validNonce(), TestKeys.key1());
-    assertProblems(cert);
+    PushCertificate cert =
+        newSignedCert(validNonce(), validKeyWithoutExpiration());
+    assertNoProblems(cert);
   }
 
   @Test
   public void invalidNonce() throws Exception {
-    PushCertificate cert = newSignedCert("invalid-nonce", TestKeys.key1());
+    PushCertificate cert =
+        newSignedCert("invalid-nonce", validKeyWithoutExpiration());
     assertProblems(cert, "Invalid nonce");
   }
 
   @Test
   public void invalidNonceNotChecked() throws Exception {
     checker = newChecker(false);
-    PushCertificate cert = newSignedCert("invalid-nonce", TestKeys.key1());
-    assertProblems(cert);
+    PushCertificate cert =
+        newSignedCert("invalid-nonce", validKeyWithoutExpiration());
+    assertNoProblems(cert);
   }
 
   @Test
   public void missingKey() throws Exception {
-    TestKey key2 = TestKeys.key2();
+    TestKey key2 = validKeyWithExpiration();
     PushCertificate cert = newSignedCert(validNonce(), key2);
     assertProblems(cert,
         "No public keys found for key ID " + keyIdToString(key2.getKeyId()));
@@ -112,20 +129,34 @@
 
   @Test
   public void invalidKey() throws Exception {
-    TestKey key3 = TestKeys.key3();
+    TestKey key3 = expiredKey();
     PushCertificate cert = newSignedCert(validNonce(), key3);
     assertProblems(cert,
         "Invalid public key " + keyToString(key3.getPublicKey())
           + ":\n  Key is expired");
   }
 
+  @Test
+  public void signatureByExpiredKeyBeforeExpiration() throws Exception {
+    TestKey key3 = expiredKey();
+    Date now = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss Z")
+        .parse("2005-07-10 12:00:00 -0400");
+    PushCertificate cert = newSignedCert(validNonce(), key3, now);
+    assertNoProblems(cert);
+  }
+
   private String validNonce() {
     return signedPushConfig.getNonceGenerator()
-        .createNonce(tr.getRepository(), System.currentTimeMillis() / 1000);
+        .createNonce(repo, System.currentTimeMillis() / 1000);
   }
 
   private PushCertificate newSignedCert(String nonce, TestKey signingKey)
       throws Exception {
+    return newSignedCert(nonce, signingKey, null);
+  }
+
+  private PushCertificate newSignedCert(String nonce, TestKey signingKey,
+      Date now) throws Exception {
     PushCertificateIdent ident = new PushCertificateIdent(
         signingKey.getFirstUserId(), System.currentTimeMillis(), -7 * 60);
     String payload = "certificate version 0.1\n"
@@ -139,6 +170,14 @@
     PGPSignatureGenerator gen = new PGPSignatureGenerator(
         new BcPGPContentSignerBuilder(
           signingKey.getPublicKey().getAlgorithm(), PGPUtil.SHA1));
+
+    if (now != null) {
+      PGPSignatureSubpacketGenerator subGen =
+          new PGPSignatureSubpacketGenerator();
+      subGen.setSignatureCreationTime(false, now);
+      gen.setHashedSubpackets(subGen.generate());
+    }
+
     gen.init(PGPSignature.BINARY_DOCUMENT, signingKey.getPrivateKey());
     gen.update(payload.getBytes(UTF_8));
     PGPSignature sig = gen.generate();
@@ -153,13 +192,21 @@
     Reader reader =
         new InputStreamReader(new ByteArrayInputStream(cert.getBytes(UTF_8)));
     PushCertificateParser parser =
-        new PushCertificateParser(tr.getRepository(), signedPushConfig);
+        new PushCertificateParser(repo, signedPushConfig);
     return parser.parse(reader);
   }
 
-  private void assertProblems(PushCertificate cert, String... expected)
-      throws Exception {
+  private void assertProblems(PushCertificate cert, String first,
+      String... rest) throws Exception {
+    List<String> expected = new ArrayList<>();
+    expected.add(first);
+    expected.addAll(Arrays.asList(rest));
     CheckResult result = checker.check(cert).getCheckResult();
-    assertEquals(Arrays.asList(expected), result.getProblems());
+    assertEquals(expected, result.getProblems());
+  }
+
+  private void assertNoProblems(PushCertificate cert) {
+    CheckResult result = checker.check(cert).getCheckResult();
+    assertEquals(Collections.emptyList(), result.getProblems());
   }
 }
diff --git a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKeys.java b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKeys.java
index 3abe689..ad944c5 100644
--- a/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKeys.java
+++ b/gerrit-gpg/src/test/java/com/google/gerrit/gpg/testutil/TestKeys.java
@@ -20,9 +20,9 @@
 public class TestKeys {
   public static ImmutableList<TestKey> allValidKeys() {
     return ImmutableList.of(
-        TestKeys.key1(),
-        TestKeys.key2(),
-        TestKeys.key5());
+        validKeyWithoutExpiration(),
+        validKeyWithExpiration(),
+        validKeyWithSecondUserId());
   }
 
   /**
@@ -35,7 +35,7 @@
    * sub   2048R/F0AF69C0 2015-07-08
    * </pre>
    */
-  public static TestKey key1() {
+  public static TestKey validKeyWithoutExpiration() {
     return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
         + "Version: GnuPG v1\n"
         + "\n"
@@ -135,7 +135,7 @@
    * sub   2048R/46D4F204 2015-07-08 [expires: 2065-06-25]
    * </pre>
    */
-  public static final TestKey key2() {
+  public static final TestKey validKeyWithExpiration() {
     return new TestKey(
         "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
         + "Version: GnuPG v1\n"
@@ -236,7 +236,7 @@
    * uid                  Testuser Three &lt;test3@example.com&gt;
    * </pre>
    */
-  public static final TestKey key3() {
+  public static final TestKey expiredKey() {
     return new TestKey(
         "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
         + "Version: GnuPG v1\n"
@@ -337,7 +337,7 @@
    * uid                  Testuser Four &lt;test4@example.com&gt;
    * </pre>
    */
-  public static final TestKey key4() {
+  public static final TestKey selfRevokedKey() {
     return new TestKey(
         "-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
         + "Version: GnuPG v1\n"
@@ -445,7 +445,7 @@
    * sub   2048R/C781A9E3 2015-07-30
    * </pre>
    */
-  public static TestKey key5() {
+  public static TestKey validKeyWithSecondUserId() {
     return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
         + "Version: GnuPG v1\n"
         + "\n"
@@ -542,6 +542,487 @@
         + "-----END PGP PRIVATE KEY BLOCK-----\n");
   }
 
-  // TODO(dborowitz): Figure out how to get gpg to revoke a key for someone
-  // else.
+  /**
+   * A key revoked by a valid key, due to key compromise.
+   * <p>
+   * Revoked by {@link #validKeyWithoutExpiration()}.
+   *
+   * <pre>
+   * pub   2048R/3434B39F 2015-10-20 [revoked: 2015-10-20]
+   *       Key fingerprint = 931F 047D 7D01 DDEF 367A  8D90 8C4F D28E 3434 B39F
+   * uid                  Testuser Six &lt;test6@example.com&gt;
+   * </pre>
+   */
+  public static TestKey revokedCompromisedKey() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "tULvIEIjNNP8Qizfc4u2O8h7HP2b3yYSrp9MMQ==\n"
+        + "=Dxr7\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "i7Y7yHsc/ZvfJhKun0wx\n"
+        + "=M/kw\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * A key revoked by a valid key, due to no longer being used.
+   * <p>
+   * Revoked by {@link #validKeyWithoutExpiration()}.
+   *
+   * <pre>
+   * pub   2048R/3D6C52D0 2015-10-20 [revoked: 2015-10-20]
+   *       Key fingerprint = 32DB 6C31 2ED7 A98D 11B2  43EA FAD2 ABE2 3D6C 52D0
+   * uid                  Testuser Seven &lt;test7@example.com&gt;
+   * </pre>
+   */
+  public static TestKey revokedNoLongerUsedKey() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "+B6paw/s8P7XCWznTBRdlFWWgrhcpzQ8fefC\n"
+        + "=CHer\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "bOdMFF2UVZaCuFynNDx958I=\n"
+        + "=aoJv\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * Key revoked by an expired key, after that key's expiration.
+   * <p>
+   * Revoked by {@link #expiredKey()}.
+   *
+   * <pre>
+   * pub   2048R/78BF7D7E 2005-08-01 [revoked: 2015-10-20]
+   *       Key fingerprint = 916F AB22 5BE7 7585 F59A  994C 001A DF8B 78BF 7D7E
+   * uid                  Testuser Eight &lt;test8@example.com&gt;
+   * </pre>
+   */
+  public static TestKey keyRevokedByExpiredKeyAfterExpiration() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "0d0PyzZT3676mQwmFoebMR9vACAeHjvDxD4F\n"
+        + "=ihWb\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "DCYWh5sxH28AIB4eO8PEPgU=\n"
+        + "=cSfw\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
+
+  /**
+   * Key revoked by an expired key, before that key's expiration.
+   * <p>
+   * Revoked by {@link #expiredKey()}.
+   *
+   * <pre>
+   * pub   2048R/C43BF2E1 2005-08-01 [revoked: 2005-08-01]
+   *       Key fingerprint = 916D 6AD6 36A5 CBA6 B5A6  7274 6040 8661 C43B F2E1
+   * uid                  Testuser Nine &lt;test9@example.com&gt;
+   * </pre>
+   */
+  public static TestKey keyRevokedByExpiredKeyBeforeExpiration() throws Exception {
+    return new TestKey("-----BEGIN PGP PUBLIC KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "REW68wsnWjwTtrPG8bqo6cCsOzqGYVt81eU=\n"
+        + "=FnZg\n"
+        + "-----END PGP PUBLIC KEY BLOCK-----\n",
+        "-----BEGIN PGP PRIVATE KEY BLOCK-----\n"
+        + "Version: GnuPG v1\n"
+        + "\n"
+        + "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\n"
+        + "s8bxuqjpwKw7OoZhW3zV5Q==\n"
+        + "=JxsF\n"
+        + "-----END PGP PRIVATE KEY BLOCK-----\n");
+  }
 }
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
index 5da8b1e..4b6e7e4 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/clippy/client/CopyableLabel.java
@@ -274,6 +274,7 @@
     try {
       t.setText(getText());
       content.add(t);
+      t.setFocus(true);
       t.selectAll();
 
       boolean ok = execCommand("copy");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
index 08fe75d..c77b71f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
@@ -109,17 +109,28 @@
     return new AccountFormatter(Gerrit.info().user().anonymousCowardName());
   }
 
+  /** The returned format string doesn't contain any +/- sign. */
+  public static String formatAbsBytes(long bytes) {
+    return formatBytes(bytes, true);
+  }
+
   public static String formatBytes(long bytes) {
+    return formatBytes(bytes, false);
+  }
+
+  private static String formatBytes(long bytes, boolean abs) {
+    bytes = abs ? Math.abs(bytes) : bytes;
+
     if (bytes == 0) {
-      return "+/- 0 B";
+      return abs ? "0 B" : "+/- 0 B";
     }
 
     if (Math.abs(bytes) < 1024) {
-      return (bytes > 0 ? "+" : "") + bytes + " B";
+      return (bytes > 0 && !abs ? "+" : "") + bytes + " B";
     }
 
     int exp = (int) (Math.log(Math.abs(bytes)) / Math.log(1024));
-    return (bytes > 0 ? "+" : "")
+    return (bytes > 0 && !abs ? "+" : "")
         + NumberFormat.getFormat("#.0").format(bytes / Math.pow(1024, exp))
         + " " + "KMGTPE".charAt(exp - 1) + "iB";
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
index cd53b8e..ee67116 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Gerrit.java
@@ -47,9 +47,9 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.HostPageData;
 import com.google.gerrit.common.data.SystemInfoService;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.client.GerritTopMenu;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.aria.client.Roles;
 import com.google.gwt.core.client.EntryPoint;
@@ -119,7 +119,7 @@
   private static String docUrl;
   private static HostPageData.Theme myTheme;
   private static String defaultScreenToken;
-  private static AccountDiffPreference myAccountDiffPref;
+  private static DiffPreferencesInfo myAccountDiffPref;
   private static EditPreferencesInfo editPrefs;
   private static String xGerritAuth;
   private static boolean isNoteDbEnabled;
@@ -321,12 +321,12 @@
     return myPrefs;
   }
 
-  /** @return the currently signed in users's diff preferences; null if no diff preferences defined for the account */
-  public static AccountDiffPreference getAccountDiffPreference() {
+  /** @return the currently signed in users's diff preferences, or default values */
+  public static DiffPreferencesInfo getDiffPreferences() {
     return myAccountDiffPref;
   }
 
-  public static void setAccountDiffPreference(AccountDiffPreference accountDiffPref) {
+  public static void setDiffPreferences(DiffPreferencesInfo accountDiffPref) {
     myAccountDiffPref = accountDiffPref;
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
index 54541e4..07d45e9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/DiffPreferences.java
@@ -14,55 +14,58 @@
 
 package com.google.gerrit.client.account;
 
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.Theme;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gwt.core.client.JavaScriptObject;
 
 public class DiffPreferences extends JavaScriptObject {
-  public static DiffPreferences create(AccountDiffPreference in) {
+  public static DiffPreferences create(DiffPreferencesInfo in) {
     DiffPreferences p = createObject().cast();
-    if (in == null) {
-      in = AccountDiffPreference.createDefault(null);
-    }
-    p.ignoreWhitespace(in.getIgnoreWhitespace());
-    p.tabSize(in.getTabSize());
-    p.lineLength(in.getLineLength());
-    p.context(in.getContext());
-    p.intralineDifference(in.isIntralineDifference());
-    p.showLineEndings(in.isShowLineEndings());
-    p.showTabs(in.isShowTabs());
-    p.showWhitespaceErrors(in.isShowWhitespaceErrors());
-    p.syntaxHighlighting(in.isSyntaxHighlighting());
-    p.hideTopMenu(in.isHideTopMenu());
-    p.autoHideDiffTableHeader(in.isAutoHideDiffTableHeader());
-    p.hideLineNumbers(in.isHideLineNumbers());
-    p.expandAllComments(in.isExpandAllComments());
-    p.manualReview(in.isManualReview());
-    p.renderEntireFile(in.isRenderEntireFile());
-    p.theme(in.getTheme());
-    p.hideEmptyPane(in.isHideEmptyPane());
+    p.ignoreWhitespace(in.ignoreWhitespace);
+    p.tabSize(in.tabSize);
+    p.lineLength(in.lineLength);
+    p.context(in.context);
+    p.intralineDifference(in.intralineDifference);
+    p.showLineEndings(in.showLineEndings);
+    p.showTabs(in.showTabs);
+    p.showWhitespaceErrors(in.showWhitespaceErrors);
+    p.syntaxHighlighting(in.syntaxHighlighting);
+    p.hideTopMenu(in.hideTopMenu);
+    p.autoHideDiffTableHeader(in.autoHideDiffTableHeader);
+    p.hideLineNumbers(in.hideLineNumbers);
+    p.expandAllComments(in.expandAllComments);
+    p.manualReview(in.manualReview);
+    p.renderEntireFile(in.renderEntireFile);
+    p.theme(in.theme);
+    p.hideEmptyPane(in.hideEmptyPane);
+    p.retainHeader(in.retainHeader);
+    p.skipUncommented(in.skipUncommented);
+    p.skipDeleted(in.skipDeleted);
     return p;
   }
 
-  public final void copyTo(AccountDiffPreference p) {
-    p.setIgnoreWhitespace(ignoreWhitespace());
-    p.setTabSize(tabSize());
-    p.setLineLength(lineLength());
-    p.setContext((short)context());
-    p.setIntralineDifference(intralineDifference());
-    p.setShowLineEndings(showLineEndings());
-    p.setShowTabs(showTabs());
-    p.setShowWhitespaceErrors(showWhitespaceErrors());
-    p.setSyntaxHighlighting(syntaxHighlighting());
-    p.setHideTopMenu(hideTopMenu());
-    p.setAutoHideDiffTableHeader(autoHideDiffTableHeader());
-    p.setHideLineNumbers(hideLineNumbers());
-    p.setExpandAllComments(expandAllComments());
-    p.setManualReview(manualReview());
-    p.setRenderEntireFile(renderEntireFile());
-    p.setTheme(theme());
-    p.setHideEmptyPane(hideEmptyPane());
+  public final void copyTo(DiffPreferencesInfo p) {
+    p.context = context();
+    p.tabSize = tabSize();
+    p.lineLength = lineLength();
+    p.expandAllComments = expandAllComments();
+    p.intralineDifference = intralineDifference();
+    p.manualReview = manualReview();
+    p.retainHeader = retainHeader();
+    p.showLineEndings = showLineEndings();
+    p.showTabs = showTabs();
+    p.showWhitespaceErrors = showWhitespaceErrors();
+    p.skipDeleted = skipDeleted();
+    p.skipUncommented = skipUncommented();
+    p.syntaxHighlighting = syntaxHighlighting();
+    p.hideTopMenu = hideTopMenu();
+    p.autoHideDiffTableHeader = autoHideDiffTableHeader();
+    p.hideLineNumbers = hideLineNumbers();
+    p.renderEntireFile = renderEntireFile();
+    p.hideEmptyPane = hideEmptyPane();
+    p.theme = theme();
+    p.ignoreWhitespace = ignoreWhitespace();
   }
 
   public final void ignoreWhitespace(Whitespace i) {
@@ -122,6 +125,9 @@
   public final native void manualReview(boolean r) /*-{ this.manual_review = r }-*/;
   public final native void renderEntireFile(boolean r) /*-{ this.render_entire_file = r }-*/;
   public final native void hideEmptyPane(boolean s) /*-{ this.hide_empty_pane = s }-*/;
+  public final native void retainHeader(boolean r) /*-{ this.retain_header = r }-*/;
+  public final native void skipUncommented(boolean s) /*-{ this.skip_uncommented = s }-*/;
+  public final native void skipDeleted(boolean s) /*-{ this.skip_deleted = s }-*/;
   public final native boolean intralineDifference() /*-{ return this.intraline_difference || false }-*/;
   public final native boolean showLineEndings() /*-{ return this.show_line_endings || false }-*/;
   public final native boolean showTabs() /*-{ return this.show_tabs || false }-*/;
@@ -134,6 +140,9 @@
   public final native boolean manualReview() /*-{ return this.manual_review || false }-*/;
   public final native boolean renderEntireFile() /*-{ return this.render_entire_file || false }-*/;
   public final native boolean hideEmptyPane() /*-{ return this.hide_empty_pane || false }-*/;
+  public final native boolean retainHeader() /*-{ return this.retain_header || false }-*/;
+  public final native boolean skipUncommented() /*-{ return this.skip_uncommented || false }-*/;
+  public final native boolean skipDeleted() /*-{ return this.skip_deleted || false }-*/;
 
   private final native void setThemeRaw(String i) /*-{ this.theme = i }-*/;
   private final native void setIgnoreWhitespaceRaw(String i) /*-{ this.ignore_whitespace = i }-*/;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyDiffPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyDiffPreferencesScreen.java
index 73d4ca0..a721441 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyDiffPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyDiffPreferencesScreen.java
@@ -25,7 +25,7 @@
     super.onInitUI();
 
     PreferencesBox pb = new PreferencesBox(null);
-    pb.set(DiffPreferences.create(Gerrit.getAccountDiffPreference()));
+    pb.set(DiffPreferences.create(Gerrit.getDiffPreferences()));
     FlowPanel p = new FlowPanel();
     p.setStyleName(pb.getStyle().dialog());
     p.add(pb);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
index f83e9ec..f6ac36a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
@@ -36,6 +36,7 @@
       protected void preDisplay(GroupList result) {
         groups.display(result);
         groups.finishDisplay();
-      }});
+      }
+    });
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
index aa72300..af0b1f5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
@@ -163,7 +163,8 @@
       @Override
       public void execute() {
         name.setFocus(true);
-      }});
+      }
+    });
   }
 
   void enableEditing() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index f0928040..511be5f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -43,6 +43,7 @@
   String useSignedOffBy();
   String createNewChangeForAllNotInTarget();
   String enableSignedPush();
+  String requireSignedPush();
   String requireChangeID();
   String headingMaxObjectSizeLimit();
   String headingGroupOptions();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index da260de..7a8888c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -25,6 +25,7 @@
 useSignedOffBy = Require <code>Signed-off-by</code> in commit message
 createNewChangeForAllNotInTarget = Create a new change for every commit not in the target branch
 enableSignedPush = Enable signed push
+requireSignedPush = Require signed push
 requireChangeID = Require <code>Change-Id</code> in commit message
 headingMaxObjectSizeLimit = Maximum Git object size limit
 headingGroupOptions = Group Options
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index eefd199..c6bd1d1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -84,6 +84,7 @@
   private ListBox contentMerge;
   private ListBox newChangeForAllNotInTarget;
   private ListBox enableSignedPush;
+  private ListBox requireSignedPush;
   private NpTextBox maxObjectSizeLimit;
   private Label effectiveMaxObjectSizeLimit;
   private Map<String, Map<String, HasEnabled>> pluginConfigWidgets;
@@ -247,6 +248,9 @@
       enableSignedPush = newInheritedBooleanBox();
       saveEnabler.listenTo(enableSignedPush);
       grid.add(Util.C.enableSignedPush(), enableSignedPush);
+      requireSignedPush = newInheritedBooleanBox();
+      saveEnabler.listenTo(requireSignedPush);
+      grid.add(Util.C.requireSignedPush(), requireSignedPush);
     }
 
     maxObjectSizeLimit = new NpTextBox();
@@ -326,6 +330,9 @@
   }
 
   private void setBool(ListBox box, InheritedBooleanInfo inheritedBoolean) {
+    if (box == null) {
+      return;
+    }
     int inheritedIndex = -1;
     for (int i = 0; i < box.getItemCount(); i++) {
       if (box.getValue(i).startsWith(InheritableBoolean.INHERIT.name())) {
@@ -372,8 +379,9 @@
     setBool(contentMerge, result.useContentMerge());
     setBool(newChangeForAllNotInTarget, result.createNewChangeForAllNotInTarget());
     setBool(requireChangeID, result.requireChangeId());
-    if (enableSignedPush != null) {
+    if (Gerrit.info().receive().enableSignedPush()) {
       setBool(enableSignedPush, result.enableSignedPush());
+      setBool(requireSignedPush, result.requireSignedPush());
     }
     setSubmitType(result.submitType());
     setState(result.state());
@@ -644,12 +652,14 @@
   private void doSave() {
     enableForm(false);
     saveProject.setEnabled(false);
-    InheritableBoolean sp = enableSignedPush != null
+    InheritableBoolean esp = enableSignedPush != null
         ? getBool(enableSignedPush) : null;
+    InheritableBoolean rsp = requireSignedPush != null
+        ? getBool(requireSignedPush) : null;
     ProjectApi.setConfig(getProjectKey(), descTxt.getText().trim(),
         getBool(contributorAgreements), getBool(contentMerge),
         getBool(signedOffBy), getBool(newChangeForAllNotInTarget), getBool(requireChangeID),
-        sp,
+        esp, rsp,
         maxObjectSizeLimit.getText().trim(),
         SubmitType.valueOf(submitType.getValue(submitType.getSelectedIndex())),
         ProjectState.valueOf(state.getValue(state.getSelectedIndex())),
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
index 932cda4..d1ca517 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/FileTable.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.client.change;
 
+import static com.google.gerrit.client.FormatUtil.formatAbsBytes;
+import static com.google.gerrit.client.FormatUtil.formatBytes;
+
 import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.changes.ChangeApi;
@@ -458,8 +460,12 @@
     private ProgressBar meter;
     private String lastPath = "";
 
+    private boolean hasBinaryFile;
+    private boolean hasNonBinaryFile;
     private int inserted;
     private int deleted;
+    private long bytesInserted;
+    private long bytesDeleted;
 
     private DisplayCommand(NativeMap<FileInfo> map,
         JsArray<FileInfo> list,
@@ -514,11 +520,23 @@
     private void computeInsertedDeleted() {
       inserted = 0;
       deleted = 0;
+      bytesInserted = 0;
+      bytesDeleted = 0;
       for (int i = 0; i < list.length(); i++) {
         FileInfo info = list.get(i);
-        if (!Patch.COMMIT_MSG.equals(info.path()) && !info.binary()) {
-          inserted += info.linesInserted();
-          deleted += info.linesDeleted();
+        if (!Patch.COMMIT_MSG.equals(info.path())) {
+          if (!info.binary()) {
+            hasNonBinaryFile = true;
+            inserted += info.linesInserted();
+            deleted += info.linesDeleted();
+          } else {
+            hasBinaryFile = true;
+            if (info.sizeDelta() >= 0) {
+              bytesInserted += info.sizeDelta();
+            } else {
+              bytesDeleted += info.sizeDelta();
+            }
+          }
         }
       }
     }
@@ -752,7 +770,7 @@
           }
         }
       } else if (info.binary()) {
-        sb.append(FormatUtil.formatBytes(info.sizeDelta()));
+        sb.append(formatBytes(info.sizeDelta()));
       }
       sb.closeTd();
     }
@@ -801,9 +819,18 @@
       sb.openTd().setAttribute("colspan", 3).closeTd(); // comments
 
       // delta1
-      sb.openTh().setStyleName(R.css().deltaColumn1())
-        .append(Util.M.patchTableSize_Modify(inserted, deleted))
-        .closeTh();
+      sb.openTh().setStyleName(R.css().deltaColumn1());
+      if (hasNonBinaryFile) {
+        sb.append(Util.M.patchTableSize_Modify(inserted, deleted));
+      }
+      if (hasBinaryFile) {
+        if (hasNonBinaryFile) {
+          sb.br();
+        }
+        sb.append(Util.M.patchTableSize_ModifyBinaryFiles(
+            formatAbsBytes(bytesInserted), formatAbsBytes(bytesDeleted)));
+      }
+      sb.closeTh();
 
       // delta2
       sb.openTh().setStyleName(R.css().deltaColumn2());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
index 626e7c0..2ec4b6b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/ReplyBox.java
@@ -158,7 +158,8 @@
       @Override
       public void execute() {
         message.setFocus(true);
-      }});
+      }
+    });
     Scheduler.get().scheduleFixedDelay(new RepeatingCommand() {
       @Override
       public boolean execute() {
@@ -167,7 +168,8 @@
           message.setCursorPos(t.length());
         }
         return false;
-      }}, 0);
+      }
+    }, 0);
   }
 
   @UiHandler("post")
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
index f0101cb..bde9755 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/file_table.css
@@ -75,7 +75,7 @@
 
 .deltaColumn1 {
   white-space: nowrap;
-  text-align: right;
+  text-align: right !important;
 }
 
 .deltaColumn2 {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index 2d3644e..ef74a65 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -34,6 +34,8 @@
   String patchTableComments(@PluralCount int count);
   String patchTableDrafts(@PluralCount int count);
   String patchTableSize_Modify(int insertions, int deletions);
+  String patchTableSize_ModifyBinaryFiles(String bytesInserted,
+      String bytesDeleted);
   String patchTableSize_LongModify(int insertions, int deletions);
   String patchTableSize_Lines(@PluralCount int insertions);
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index c109794..67ef2c3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -17,6 +17,7 @@
 patchTableComments = {0} comments
 patchTableDrafts = {0} drafts
 patchTableSize_Modify = +{0}, -{1}
+patchTableSize_ModifyBinaryFiles = +{0}, -{1}
 patchTableSize_LongModify = {0} inserted, {1} deleted
 patchTableSize_Lines = {0} lines
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
index 800da91..6d795d3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/DiffApi.java
@@ -18,7 +18,7 @@
 import com.google.gerrit.client.info.FileInfo;
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.RestApi;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
@@ -69,16 +69,16 @@
     return this;
   }
 
-  public DiffApi ignoreWhitespace(AccountDiffPreference.Whitespace w) {
+  public DiffApi ignoreWhitespace(DiffPreferencesInfo.Whitespace w) {
     switch (w) {
       default:
       case IGNORE_NONE:
         return ignoreWhitespace(IgnoreWhitespace.NONE);
-      case IGNORE_SPACE_AT_EOL:
+      case IGNORE_TRAILING:
         return ignoreWhitespace(IgnoreWhitespace.TRAILING);
-      case IGNORE_SPACE_CHANGE:
+      case IGNORE_LEADING_AND_TRAILING:
         return ignoreWhitespace(IgnoreWhitespace.CHANGED);
-      case IGNORE_ALL_SPACE:
+      case IGNORE_ALL:
         return ignoreWhitespace(IgnoreWhitespace.ALL);
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
index 3364490..a50768d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/PreferencesBox.java
@@ -16,10 +16,10 @@
 
 import static com.google.gerrit.reviewdb.client.AccountDiffPreference.DEFAULT_CONTEXT;
 import static com.google.gerrit.reviewdb.client.AccountDiffPreference.WHOLE_FILE_CONTEXT;
-import static com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace.IGNORE_ALL_SPACE;
+import static com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace.IGNORE_ALL;
+import static com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace.IGNORE_LEADING_AND_TRAILING;
 import static com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace.IGNORE_NONE;
-import static com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace.IGNORE_SPACE_AT_EOL;
-import static com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace.IGNORE_SPACE_CHANGE;
+import static com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace.IGNORE_TRAILING;
 import static com.google.gwt.event.dom.client.KeyCodes.KEY_ESCAPE;
 
 import com.google.gerrit.client.Gerrit;
@@ -29,9 +29,9 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.NpIntTextBox;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.client.Theme;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gwt.core.client.GWT;
@@ -503,12 +503,9 @@
     AccountApi.putDiffPreferences(prefs, new GerritCallback<DiffPreferences>() {
       @Override
       public void onSuccess(DiffPreferences result) {
-        AccountDiffPreference p = Gerrit.getAccountDiffPreference();
-        if (p == null) {
-          p = AccountDiffPreference.createDefault(Gerrit.getUserAccount().getId());
-        }
+        DiffPreferencesInfo p = Gerrit.getDiffPreferences();
         result.copyTo(p);
-        Gerrit.setAccountDiffPreference(p);
+        Gerrit.setDiffPreferences(p);
       }
     });
     if (view != null) {
@@ -546,14 +543,14 @@
         PatchUtil.C.whitespaceIGNORE_NONE(),
         IGNORE_NONE.name());
     ignoreWhitespace.addItem(
-        PatchUtil.C.whitespaceIGNORE_SPACE_AT_EOL(),
-        IGNORE_SPACE_AT_EOL.name());
+        PatchUtil.C.whitespaceIGNORE_TRAILING(),
+        IGNORE_TRAILING.name());
     ignoreWhitespace.addItem(
-        PatchUtil.C.whitespaceIGNORE_SPACE_CHANGE(),
-        IGNORE_SPACE_CHANGE.name());
+        PatchUtil.C.whitespaceIGNORE_LEADING_AND_TRAILING(),
+        IGNORE_LEADING_AND_TRAILING.name());
     ignoreWhitespace.addItem(
-        PatchUtil.C.whitespaceIGNORE_ALL_SPACE(),
-        IGNORE_ALL_SPACE.name());
+        PatchUtil.C.whitespaceIGNORE_ALL(),
+        IGNORE_ALL.name());
   }
 
   private void initMode() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
index 6ff93e4..7fc84f0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SideBySide.java
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.client.diff;
 
-import static com.google.gerrit.reviewdb.client.AccountDiffPreference.WHOLE_FILE_CONTEXT;
+import static com.google.gerrit.extensions.client.DiffPreferencesInfo.WHOLE_FILE_CONTEXT;
+
 import static java.lang.Double.POSITIVE_INFINITY;
 
 import com.google.gerrit.client.Dispatcher;
@@ -151,7 +152,7 @@
     this.startSide = startSide;
     this.startLine = startLine;
 
-    prefs = DiffPreferences.create(Gerrit.getAccountDiffPreference());
+    prefs = DiffPreferences.create(Gerrit.getDiffPreferences());
     handlers = new ArrayList<>(6);
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
     header = new Header(keysNavigation, base, revision, path);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
index a344e6b..5376588 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/diff/SkipManager.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.diff.DiffInfo.Region;
 import com.google.gerrit.client.patches.SkippedLine;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gwt.core.client.JsArray;
 
 import net.codemirror.lib.CodeMirror;
@@ -39,7 +39,7 @@
   }
 
   void render(int context, DiffInfo diff) {
-    if (context == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+    if (context == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) {
       return;
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
index 5cb3fbc..a546c62 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editor/EditScreen.java
@@ -518,6 +518,13 @@
         if (!cm.isClean(generation)) {
           close.setEnabled(false);
           String text = cm.getValue();
+          if (Patch.COMMIT_MSG.equals(path)) {
+            String trimmed = text.trim() + "\r";
+            if (!trimmed.equals(text)) {
+              text = trimmed;
+              cm.setValue(text);
+            }
+          }
           final int g = cm.changeGeneration(false);
           ChangeEditApi.put(revision.getParentKey().get(), path, text,
               new GerritCallback<VoidResult>() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
index 2420e7a..5b9203a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
@@ -28,11 +28,11 @@
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.prettify.client.ClientSideFormatter;
 import com.google.gerrit.prettify.client.PrettyFormatter;
 import com.google.gerrit.prettify.client.SparseHtmlFile;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -286,8 +286,8 @@
   }
 
   protected SparseHtmlFile getSparseHtmlFileA(PatchScript s) {
-    AccountDiffPreference dp = new AccountDiffPreference(s.getDiffPrefs());
-    dp.setShowWhitespaceErrors(false);
+    DiffPreferencesInfo dp = s.getDiffPrefs();
+    dp.showWhitespaceErrors = false;
 
     PrettyFormatter f = ClientSideFormatter.FACTORY.get();
     f.setDiffPrefs(dp);
@@ -299,7 +299,7 @@
   }
 
   protected SparseHtmlFile getSparseHtmlFileB(PatchScript s) {
-    AccountDiffPreference dp = new AccountDiffPreference(s.getDiffPrefs());
+    DiffPreferencesInfo dp = s.getDiffPrefs();
 
     SparseFileContent b = s.getB();
     PrettyFormatter f = ClientSideFormatter.FACTORY.get();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
index 39aadc3..422a4dd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
@@ -69,9 +69,9 @@
   String commentCancelEdit();
 
   String whitespaceIGNORE_NONE();
-  String whitespaceIGNORE_SPACE_AT_EOL();
-  String whitespaceIGNORE_SPACE_CHANGE();
-  String whitespaceIGNORE_ALL_SPACE();
+  String whitespaceIGNORE_TRAILING();
+  String whitespaceIGNORE_LEADING_AND_TRAILING();
+  String whitespaceIGNORE_ALL();
 
   String previousFileHelp();
   String nextFileHelp();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
index 2f68822..aa6177b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
@@ -50,9 +50,9 @@
 commentCancelEdit = Cancel comment edit
 
 whitespaceIGNORE_NONE=None
-whitespaceIGNORE_SPACE_AT_EOL=At Line End
-whitespaceIGNORE_SPACE_CHANGE=Leading, At Line End
-whitespaceIGNORE_ALL_SPACE=All
+whitespaceIGNORE_TRAILING=At Line End
+whitespaceIGNORE_LEADING_AND_TRAILING=Leading, At Line End
+whitespaceIGNORE_ALL=All
 
 previousFileHelp = Previous file
 nextFileHelp = Next file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
index 264e043..b7ba64b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
@@ -20,8 +20,8 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
 import com.google.gerrit.client.ui.NpIntTextBox;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.NodeList;
 import com.google.gwt.dom.client.OptionElement;
@@ -156,7 +156,7 @@
   public void setEnableSmallFileFeatures(final boolean on) {
     enableSmallFileFeatures = on;
     if (enableSmallFileFeatures) {
-      syntaxHighlighting.setValue(getValue().isSyntaxHighlighting());
+      syntaxHighlighting.setValue(getValue().syntaxHighlighting);
     } else {
       syntaxHighlighting.setValue(false);
     }
@@ -181,7 +181,7 @@
   public void setEnableIntralineDifference(final boolean on) {
     enableIntralineDifference = on;
     if (enableIntralineDifference) {
-      intralineDifference.setValue(getValue().isIntralineDifference());
+      intralineDifference.setValue(getValue().intralineDifference);
     } else {
       intralineDifference.setValue(false);
     }
@@ -197,36 +197,36 @@
     syntaxHighlighting.setTitle(title);
   }
 
-  public AccountDiffPreference getValue() {
+  public DiffPreferencesInfo getValue() {
     return listenablePrefs.get();
   }
 
-  public void setValue(final AccountDiffPreference dp) {
+  public void setValue(final DiffPreferencesInfo dp) {
     listenablePrefs.set(dp);
     display();
   }
 
   protected void display() {
-    final AccountDiffPreference dp = getValue();
-    setIgnoreWhitespace(dp.getIgnoreWhitespace());
+    final DiffPreferencesInfo dp = getValue();
+    setIgnoreWhitespace(dp.ignoreWhitespace);
     if (enableSmallFileFeatures) {
-      syntaxHighlighting.setValue(dp.isSyntaxHighlighting());
+      syntaxHighlighting.setValue(dp.syntaxHighlighting);
     } else {
       syntaxHighlighting.setValue(false);
     }
-    setContext(dp.getContext());
+    setContext(dp.context);
 
-    tabWidth.setIntValue(dp.getTabSize());
-    colWidth.setIntValue(dp.getLineLength());
-    intralineDifference.setValue(dp.isIntralineDifference());
-    whitespaceErrors.setValue(dp.isShowWhitespaceErrors());
-    showLineEndings.setValue(dp.isShowLineEndings());
-    showTabs.setValue(dp.isShowTabs());
-    skipDeleted.setValue(dp.isSkipDeleted());
-    skipUncommented.setValue(dp.isSkipUncommented());
-    expandAllComments.setValue(dp.isExpandAllComments());
-    retainHeader.setValue(dp.isRetainHeader());
-    manualReview.setValue(dp.isManualReview());
+    tabWidth.setIntValue(dp.tabSize);
+    colWidth.setIntValue(dp.lineLength);
+    intralineDifference.setValue(dp.intralineDifference);
+    whitespaceErrors.setValue(dp.showWhitespaceErrors);
+    showLineEndings.setValue(dp.showLineEndings);
+    showTabs.setValue(dp.showTabs);
+    skipDeleted.setValue(dp.skipDeleted);
+    skipUncommented.setValue(dp.skipUncommented);
+    expandAllComments.setValue(dp.expandAllComments);
+    retainHeader.setValue(dp.retainHeader);
+    manualReview.setValue(dp.manualReview);
   }
 
   @UiHandler("update")
@@ -244,22 +244,21 @@
       new ErrorDialog(PatchUtil.C.illegalNumberOfColumns()).center();
       return;
     }
-
-    AccountDiffPreference dp = new AccountDiffPreference(getValue());
-    dp.setIgnoreWhitespace(getIgnoreWhitespace());
-    dp.setContext(getContext());
-    dp.setTabSize(tabWidth.getIntValue());
-    dp.setLineLength(colWidth.getIntValue());
-    dp.setSyntaxHighlighting(syntaxHighlighting.getValue());
-    dp.setIntralineDifference(intralineDifference.getValue());
-    dp.setShowWhitespaceErrors(whitespaceErrors.getValue());
-    dp.setShowLineEndings(showLineEndings.getValue());
-    dp.setShowTabs(showTabs.getValue());
-    dp.setSkipDeleted(skipDeleted.getValue());
-    dp.setSkipUncommented(skipUncommented.getValue());
-    dp.setExpandAllComments(expandAllComments.getValue());
-    dp.setRetainHeader(retainHeader.getValue());
-    dp.setManualReview(manualReview.getValue());
+    DiffPreferencesInfo dp = getValue();
+    dp.ignoreWhitespace = getIgnoreWhitespace();
+    dp.context = getContext();
+    dp.tabSize = tabWidth.getIntValue();
+    dp.lineLength = colWidth.getIntValue();
+    dp.syntaxHighlighting = syntaxHighlighting.getValue();
+    dp.intralineDifference = intralineDifference.getValue();
+    dp.showWhitespaceErrors = whitespaceErrors.getValue();
+    dp.showLineEndings = showLineEndings.getValue();
+    dp.showTabs = showTabs.getValue();
+    dp.skipDeleted = skipDeleted.getValue();
+    dp.skipUncommented = skipUncommented.getValue();
+    dp.expandAllComments = expandAllComments.getValue();
+    dp.retainHeader = retainHeader.getValue();
+    dp.manualReview = manualReview.getValue();
 
     listenablePrefs.set(dp);
   }
@@ -289,18 +288,18 @@
   private void initIgnoreWhitespace(ListBox ws) {
     ws.addItem(PatchUtil.C.whitespaceIGNORE_NONE(), //
         Whitespace.IGNORE_NONE.name());
-    ws.addItem(PatchUtil.C.whitespaceIGNORE_SPACE_AT_EOL(), //
-        Whitespace.IGNORE_SPACE_AT_EOL.name());
-    ws.addItem(PatchUtil.C.whitespaceIGNORE_SPACE_CHANGE(), //
-        Whitespace.IGNORE_SPACE_CHANGE.name());
-    ws.addItem(PatchUtil.C.whitespaceIGNORE_ALL_SPACE(), //
-        Whitespace.IGNORE_ALL_SPACE.name());
+    ws.addItem(PatchUtil.C.whitespaceIGNORE_TRAILING(), //
+        Whitespace.IGNORE_TRAILING.name());
+    ws.addItem(PatchUtil.C.whitespaceIGNORE_LEADING_AND_TRAILING(), //
+        Whitespace.IGNORE_LEADING_AND_TRAILING.name());
+    ws.addItem(PatchUtil.C.whitespaceIGNORE_ALL(), //
+        Whitespace.IGNORE_ALL.name());
   }
 
   private void initContext(ListBox context) {
-    for (final short v : AccountDiffPreference.CONTEXT_CHOICES) {
+    for (final short v : DiffPreferencesInfo.CONTEXT_CHOICES) {
       final String label;
-      if (v == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+      if (v == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) {
         label = Util.C.contextWholeFile();
       } else {
         label = Util.M.lines(v);
@@ -314,7 +313,7 @@
     if (0 <= sel) {
       return Whitespace.valueOf(ignoreWhitespace.getValue(sel));
     }
-    return getValue().getIgnoreWhitespace();
+    return getValue().ignoreWhitespace;
   }
 
   private void setIgnoreWhitespace(Whitespace s) {
@@ -327,12 +326,12 @@
     ignoreWhitespace.setSelectedIndex(0);
   }
 
-  private short getContext() {
+  private int getContext() {
     final int sel = context.getSelectedIndex();
     if (0 <= sel) {
       return Short.parseShort(context.getValue(sel));
     }
-    return getValue().getContext();
+    return getValue().context;
   }
 
   private void setContext(int ctx) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
index f3bc092..d84f799 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTable.java
@@ -65,9 +65,9 @@
       new PatchValidator() {
         @Override
         public boolean isValid(Patch patch) {
-          return !((listenablePrefs.get().isSkipDeleted()
+          return !((listenablePrefs.get().skipDeleted
               && patch.getChangeType().equals(ChangeType.DELETED))
-              || (listenablePrefs.get().isSkipUncommented()
+              || (listenablePrefs.get().skipUncommented
               && patch.getCommentCount() == 0));
         }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
index 10c029f..b3617d5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
@@ -294,7 +294,7 @@
   private void appendImageDifferences(final PatchScript script,
       final SafeHtmlBuilder nc) {
     final boolean syntaxHighlighting =
-        script.getDiffPrefs().isSyntaxHighlighting();
+        script.getDiffPrefs().syntaxHighlighting;
     if (script.getDisplayMethodA() == DisplayMethod.IMG) {
       final String url = getUrlA();
       appendImageLine(nc, url, syntaxHighlighting, false);
@@ -310,7 +310,7 @@
     final SparseHtmlFile a = getSparseHtmlFileA(script);
     final SparseHtmlFile b = getSparseHtmlFileB(script);
     final boolean syntaxHighlighting =
-        script.getDiffPrefs().isSyntaxHighlighting();
+        script.getDiffPrefs().syntaxHighlighting;
     for (final EditList.Hunk hunk : script.getHunks()) {
       appendHunkHeader(nc, hunk);
       while (hunk.next()) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
index bfa308a..e587aac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedPatchScreen.java
@@ -31,9 +31,9 @@
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.prettify.client.ClientSideFormatter;
 import com.google.gerrit.prettify.client.PrettyFactory;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.Scheduler;
@@ -125,10 +125,10 @@
     lastScript = null;
   }
 
-  private void update(AccountDiffPreference dp) {
+  private void update(DiffPreferencesInfo dp) {
     // Did the user just turn on auto-review?
-    if (!reviewedPanels.getValue() && prefs.getOld().isManualReview()
-        && !dp.isManualReview()) {
+    if (!reviewedPanels.getValue() && prefs.getOld().manualReview
+        && !dp.manualReview) {
       reviewedPanels.setValue(true);
       reviewedPanels.setReviewedByCurrentUser(true);
     }
@@ -152,25 +152,25 @@
     }
   }
 
-  private boolean canReuse(AccountDiffPreference dp, PatchScript last) {
-    if (last.getDiffPrefs().getIgnoreWhitespace() != dp.getIgnoreWhitespace()) {
+  private boolean canReuse(DiffPreferencesInfo dp, PatchScript last) {
+    if (last.getDiffPrefs().ignoreWhitespace != dp.ignoreWhitespace) {
       // Whitespace ignore setting requires server computation.
       return false;
     }
 
-    final int ctx = dp.getContext();
-    if (ctx == AccountDiffPreference.WHOLE_FILE_CONTEXT && !last.getA().isWholeFile()) {
+    final int ctx = dp.context;
+    if (ctx == DiffPreferencesInfo.WHOLE_FILE_CONTEXT
+        && !last.getA().isWholeFile()) {
       // We don't have the entire file here, so we can't render it.
       return false;
     }
 
-    if (last.getDiffPrefs().getContext() < ctx && !last.getA().isWholeFile()) {
+    if (last.getDiffPrefs().context < ctx && !last.getA().isWholeFile()) {
       // We don't have sufficient context.
       return false;
     }
 
-    if (dp.isSyntaxHighlighting()
-        && !last.getA().isWholeFile()) {
+    if (dp.syntaxHighlighting && !last.getA().isWholeFile()) {
       // We need the whole file to syntax highlight accurately.
       return false;
     }
@@ -425,15 +425,15 @@
     }
 
     if (script.isHugeFile()) {
-      AccountDiffPreference dp = script.getDiffPrefs();
-      int context = dp.getContext();
-      if (context == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+      DiffPreferencesInfo dp = script.getDiffPrefs();
+      int context = dp.context;
+      if (context == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) {
         context = Short.MAX_VALUE;
       } else if (context > Short.MAX_VALUE) {
         context = Short.MAX_VALUE;
       }
-      dp.setContext((short) Math.min(context, LARGE_FILE_CONTEXT));
-      dp.setSyntaxHighlighting(false);
+      dp.context = Math.min(context, LARGE_FILE_CONTEXT);
+      dp.syntaxHighlighting = false;
       script.setDiffPrefs(dp);
     }
 
@@ -453,7 +453,7 @@
 
     if (Gerrit.isSignedIn()) {
       boolean isReviewed = false;
-      if (isFirst && !prefs.get().isManualReview()) {
+      if (isFirst && !prefs.get().manualReview) {
         isReviewed = true;
         reviewedPanels.setReviewedByCurrentUser(isReviewed);
       } else {
@@ -476,9 +476,9 @@
     super.onShowView();
     if (prefsHandler == null) {
       prefsHandler = prefs.addValueChangeHandler(
-          new ValueChangeHandler<AccountDiffPreference>() {
+          new ValueChangeHandler<DiffPreferencesInfo>() {
             @Override
-            public void onValueChange(ValueChangeEvent<AccountDiffPreference> event) {
+            public void onValueChange(ValueChangeEvent<DiffPreferencesInfo> event) {
               update(event.getValue());
             }
           });
@@ -491,7 +491,7 @@
       new ErrorDialog(PatchUtil.C.intralineTimeout()).setText(
           Gerrit.C.warnTitle()).show();
     }
-    if (topView != null && prefs.get().isRetainHeader()) {
+    if (topView != null && prefs.get().retainHeader) {
       setTopView(topView);
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
index 3f3875a..322a354 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ConfigInfo.java
@@ -53,6 +53,9 @@
   public final native InheritedBooleanInfo enableSignedPush()
   /*-{ return this.enable_signed_push; }-*/;
 
+  public final native InheritedBooleanInfo requireSignedPush()
+  /*-{ return this.require_signed_push; }-*/;
+
   public final SubmitType submitType() {
     return SubmitType.valueOf(submitTypeRaw());
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
index f9c3640..fffdd3f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -117,6 +117,7 @@
       InheritableBoolean createNewChangeForAllNotInTarget,
       InheritableBoolean requireChangeId,
       InheritableBoolean enableSignedPush,
+      InheritableBoolean requireSignedPush,
       String maxObjectSizeLimit,
       SubmitType submitType, ProjectState state,
       Map<String, Map<String, ConfigParameterValue>> pluginConfigValues,
@@ -131,6 +132,9 @@
     if (enableSignedPush != null) {
       in.setEnableSignedPush(enableSignedPush);
     }
+    if (requireSignedPush != null) {
+      in.setRequireSignedPush(requireSignedPush);
+    }
     in.setMaxObjectSizeLimit(maxObjectSizeLimit);
     in.setSubmitType(submitType);
     in.setState(state);
@@ -257,6 +261,12 @@
     private final native void setEnableSignedPushRaw(String v)
     /*-{ if(v)this.enable_signed_push=v; }-*/;
 
+    final void setRequireSignedPush(InheritableBoolean v) {
+      setRequireSignedPushRaw(v.name());
+    }
+    private final native void setRequireSignedPushRaw(String v)
+    /*-{ if(v)this.require_signed_push=v; }-*/;
+
     final native void setMaxObjectSizeLimit(String l)
     /*-{ if(l)this.max_object_size_limit=l; }-*/;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java
index 27bc107..0c31f41 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java
@@ -17,11 +17,11 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.account.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gwtjsonrpc.common.VoidResult;
 
 public class ListenableAccountDiffPreference
-    extends ListenableOldValue<AccountDiffPreference> {
+    extends ListenableOldValue<DiffPreferencesInfo> {
 
   public ListenableAccountDiffPreference() {
     reset();
@@ -33,7 +33,7 @@
           new GerritCallback<VoidResult>() {
         @Override
         public void onSuccess(VoidResult result) {
-          Gerrit.setAccountDiffPreference(get());
+          Gerrit.setDiffPreferences(get());
           cb.onSuccess(result);
         }
 
@@ -46,10 +46,10 @@
   }
 
   public void reset() {
-    if (Gerrit.isSignedIn() && Gerrit.getAccountDiffPreference() != null) {
-      set(Gerrit.getAccountDiffPreference());
+    if (Gerrit.isSignedIn() && Gerrit.getDiffPreferences() != null) {
+      set(Gerrit.getDiffPreferences());
     } else {
-      set(AccountDiffPreference.createDefault(null));
+      set(DiffPreferencesInfo.defaults());
     }
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index c1a113f..389dcc7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -24,13 +24,18 @@
 import com.google.common.primitives.Bytes;
 import com.google.gerrit.common.Version;
 import com.google.gerrit.common.data.HostPageData;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.systemstatus.MessageOfTheDay;
 import com.google.gerrit.extensions.webui.WebUiPlugin;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.GetDiffPreferences;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -38,10 +43,12 @@
 import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtjsonrpc.server.JsonServlet;
 import com.google.gwtjsonrpc.server.RPCServletUtils;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -87,6 +94,7 @@
   private final StaticServlet staticServlet;
   private final boolean isNoteDbEnabled;
   private final Integer pluginsLoadTimeout;
+  private final GetDiffPreferences getDiff;
   private volatile Page page;
 
   @Inject
@@ -100,7 +108,8 @@
       DynamicSet<MessageOfTheDay> motd,
       @GerritServerConfig Config cfg,
       StaticServlet ss,
-      NotesMigration migration)
+      NotesMigration migration,
+      GetDiffPreferences diffPref)
       throws IOException, ServletException {
     currentUser = cu;
     session = w;
@@ -113,6 +122,7 @@
     staticServlet = ss;
     isNoteDbEnabled = migration.enabled();
     pluginsLoadTimeout = getPluginsLoadTimeout(cfg);
+    getDiff = diffPref;
 
     final String pageName = "HostPage.html";
     template = HtmlDomUtil.parseFile(getClass(), pageName);
@@ -187,7 +197,7 @@
       w.write(";");
 
       w.write(HPD_ID + ".accountDiffPref=");
-      json(user.asIdentifiedUser().getAccountDiffPreference(), w);
+      json(getDiffPreferences(user.asIdentifiedUser()), w);
       w.write(";");
 
       w.write(HPD_ID + ".theme=");
@@ -220,6 +230,16 @@
     }
   }
 
+  private DiffPreferencesInfo getDiffPreferences(IdentifiedUser user) {
+    try {
+      return getDiff.apply(new AccountResource(user));
+    } catch (AuthException | OrmException | ConfigInvalidException
+        | IOException e) {
+      log.warn("Cannot query account diff preferences", e);
+    }
+    return DiffPreferencesInfo.defaults();
+  }
+
   private void plugins(StringWriter w) {
     List<String> urls = Lists.newArrayList();
     for (WebUiPlugin u : plugins) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
index af20ea5..3b064e2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
@@ -166,14 +166,18 @@
   }
 
   private String extractWhat(final Audit note, final GerritCall call) {
-    String methodClass = call.getMethodClass().getName();
-    methodClass = methodClass.substring(methodClass.lastIndexOf(".")+1);
+    Class<?> methodClass = call.getMethodClass();
+    String methodClassName = methodClass != null
+        ? methodClass.getName()
+        : "<UNKNOWN_CLASS>";
+    methodClassName =
+        methodClassName.substring(methodClassName.lastIndexOf(".") + 1);
     String what = note.action();
     if (what.length() == 0) {
       what = call.getMethod().getName();
     }
 
-    return methodClass + "." + what;
+    return methodClassName + "." + what;
   }
 
   static class GerritCall extends ActiveCall {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
index 968029c..0da66bf 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
@@ -19,13 +19,17 @@
 import com.google.gerrit.common.data.AgreementInfo;
 import com.google.gerrit.common.errors.InvalidQueryException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.SetDiffPreferences;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.query.QueryParseException;
@@ -37,6 +41,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -49,18 +56,21 @@
   private final ProjectControl.Factory projectControlFactory;
   private final AgreementInfoFactory.Factory agreementInfoFactory;
   private final ChangeQueryBuilder queryBuilder;
+  private final SetDiffPreferences setDiff;
 
   @Inject
   AccountServiceImpl(final Provider<ReviewDb> schema,
       final Provider<IdentifiedUser> identifiedUser,
       final ProjectControl.Factory projectControlFactory,
       final AgreementInfoFactory.Factory agreementInfoFactory,
-      final ChangeQueryBuilder queryBuilder) {
+      final ChangeQueryBuilder queryBuilder,
+      SetDiffPreferences setDiff) {
     super(schema, identifiedUser);
     this.currentUser = identifiedUser;
     this.projectControlFactory = projectControlFactory;
     this.agreementInfoFactory = agreementInfoFactory;
     this.queryBuilder = queryBuilder;
+    this.setDiff = setDiff;
   }
 
   @Override
@@ -74,17 +84,21 @@
   }
 
   @Override
-  public void changeDiffPreferences(final AccountDiffPreference diffPref,
+  public void changeDiffPreferences(final DiffPreferencesInfo diffPref,
       AsyncCallback<VoidResult> callback) {
     run(callback, new Action<VoidResult>(){
       @Override
       public VoidResult run(ReviewDb db) throws OrmException {
-        if (!diffPref.getAccountId().equals(getAccountId())) {
-          throw new IllegalArgumentException("diffPref.getAccountId() "
-              + diffPref.getAccountId() + " doesn't match"
-              + " the accountId of the signed in user " + getAccountId());
+        if (!getUser().isIdentifiedUser()) {
+          throw new IllegalArgumentException("Not authenticated");
         }
-        db.accountDiffPreferences().upsert(Collections.singleton(diffPref));
+        IdentifiedUser me = getUser().asIdentifiedUser();
+        try {
+          setDiff.apply(new AccountResource(me), diffPref);
+        } catch (AuthException | BadRequestException | ConfigInvalidException
+            | IOException e) {
+          throw new OrmException("Cannot save diff preferences", e);
+        }
         return VoidResult.INSTANCE;
       }
     });
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
index d0c042c..37ca524 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.common.data.ChangeDetailService;
 import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.inject.Inject;
@@ -38,7 +38,7 @@
 
   @Override
   public void patchSetDetail2(PatchSet.Id baseId, PatchSet.Id id,
-      AccountDiffPreference diffPrefs, AsyncCallback<PatchSetDetail> callback) {
+      DiffPreferencesInfo diffPrefs, AsyncCallback<PatchSetDetail> callback) {
     patchSetDetail.create(baseId, id, diffPrefs).to(callback);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
index 2bf5fe4..d2dc384 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
@@ -18,11 +18,11 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.AccountPatchReview;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -67,7 +67,7 @@
     PatchSetDetailFactory create(
         @Assisted("psIdBase") @Nullable PatchSet.Id psIdBase,
         @Assisted("psIdNew") PatchSet.Id psIdNew,
-        @Nullable AccountDiffPreference diffPrefs);
+        @Nullable DiffPreferencesInfo diffPrefs);
   }
 
   private final PatchSetInfoFactory infoFactory;
@@ -81,7 +81,7 @@
   private Project.NameKey project;
   private final PatchSet.Id psIdBase;
   private final PatchSet.Id psIdNew;
-  private final AccountDiffPreference diffPrefs;
+  private final DiffPreferencesInfo diffPrefs;
   private ObjectId oldId;
   private ObjectId newId;
 
@@ -98,7 +98,7 @@
       ChangeEditUtil editUtil,
       @Assisted("psIdBase") @Nullable final PatchSet.Id psIdBase,
       @Assisted("psIdNew") final PatchSet.Id psIdNew,
-      @Assisted @Nullable final AccountDiffPreference diffPrefs) {
+      @Assisted @Nullable final DiffPreferencesInfo diffPrefs) {
     this.infoFactory = psif;
     this.db = db;
     this.patchListCache = patchListCache;
@@ -145,7 +145,7 @@
           newId = toObjectId(psIdNew);
         }
 
-        list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
+        list = listFor(keyFor(diffPrefs.ignoreWhitespace));
       } else { // OK, means use base to compare
         list = patchListCache.get(control.getChange(), patchSet);
       }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
index 7ff3782..f9180f7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
@@ -17,9 +17,9 @@
 import com.google.gerrit.common.data.PatchDetailService;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -48,7 +48,7 @@
 
   @Override
   public void patchScript(final Patch.Key patchKey, final PatchSet.Id psa,
-      final PatchSet.Id psb, final AccountDiffPreference dp,
+      final PatchSet.Id psb, final DiffPreferencesInfo dp,
       final AsyncCallback<PatchScript> callback) {
     if (psb == null) {
       callback.onFailure(new NoSuchEntityException());
diff --git a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index 99003ee..5c3b629 100644
--- a/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/gerrit-lucene/src/main/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -18,8 +18,8 @@
 import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.collect.Iterables.getOnlyElement;
 import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
-import static com.google.gerrit.server.index.IndexRewriteImpl.CLOSED_STATUSES;
-import static com.google.gerrit.server.index.IndexRewriteImpl.OPEN_STATUSES;
+import static com.google.gerrit.server.index.IndexRewriter.CLOSED_STATUSES;
+import static com.google.gerrit.server.index.IndexRewriter.OPEN_STATUSES;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
 
@@ -47,7 +47,7 @@
 import com.google.gerrit.server.index.FieldDef.FillArgs;
 import com.google.gerrit.server.index.FieldType;
 import com.google.gerrit.server.index.IndexExecutor;
-import com.google.gerrit.server.index.IndexRewriteImpl;
+import com.google.gerrit.server.index.IndexRewriter;
 import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.index.Schema.Values;
 import com.google.gerrit.server.query.Predicate;
@@ -55,6 +55,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
 import com.google.gerrit.server.query.change.LegacyChangeIdPredicate;
+import com.google.gerrit.server.query.change.QueryOptions;
 import com.google.gwtorm.protobuf.ProtobufCodec;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
@@ -352,9 +353,9 @@
   }
 
   @Override
-  public ChangeDataSource getSource(Predicate<ChangeData> p, int start,
-      int limit) throws QueryParseException {
-    Set<Change.Status> statuses = IndexRewriteImpl.getPossibleStatus(p);
+  public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts)
+      throws QueryParseException {
+    Set<Change.Status> statuses = IndexRewriter.getPossibleStatus(p);
     List<SubIndex> indexes = Lists.newArrayListWithCapacity(2);
     if (!Sets.intersection(statuses, OPEN_STATUSES).isEmpty()) {
       indexes.add(openIndex);
@@ -362,7 +363,7 @@
     if (!Sets.intersection(statuses, CLOSED_STATUSES).isEmpty()) {
       indexes.add(closedIndex);
     }
-    return new QuerySource(indexes, queryBuilder.toQuery(p), start, limit,
+    return new QuerySource(indexes, queryBuilder.toQuery(p), opts,
         getSort());
   }
 
@@ -397,16 +398,14 @@
   private class QuerySource implements ChangeDataSource {
     private final List<SubIndex> indexes;
     private final Query query;
-    private final int start;
-    private final int limit;
+    private final QueryOptions opts;
     private final Sort sort;
 
-    private QuerySource(List<SubIndex> indexes, Query query, int start,
-        int limit, Sort sort) {
+    private QuerySource(List<SubIndex> indexes, Query query, QueryOptions opts,
+        Sort sort) {
       this.indexes = indexes;
       this.query = checkNotNull(query, "null query from Lucene");
-      this.start = start;
-      this.limit = limit;
+      this.opts = opts;
       this.sort = sort;
     }
 
@@ -429,7 +428,7 @@
     public ResultSet<ChangeData> read() throws OrmException {
       IndexSearcher[] searchers = new IndexSearcher[indexes.size()];
       try {
-        int realLimit = start + limit;
+        int realLimit = opts.start() + opts.limit();
         TopFieldDocs[] hits = new TopFieldDocs[indexes.size()];
         for (int i = 0; i < indexes.size(); i++) {
           searchers[i] = indexes.get(i).acquire();
@@ -439,7 +438,7 @@
 
         List<ChangeData> result =
             Lists.newArrayListWithCapacity(docs.scoreDocs.length);
-        for (int i = start; i < docs.scoreDocs.length; i++) {
+        for (int i = opts.start(); i < docs.scoreDocs.length; i++) {
           ScoreDoc sd = docs.scoreDocs[i];
           Document doc = searchers[sd.shardIndex].doc(sd.doc, FIELDS);
           result.add(toChangeData(doc));
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
index 1c70468..cd1c7a3 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/BaseInit.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.pgm.init.api.InstallPlugins;
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -222,6 +223,7 @@
       throw die(err);
     }
 
+    m.add(new GerritServerConfigModule());
     m.add(new InitModule(standalone, initDb));
     m.add(new AbstractModule() {
       @Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
index 6eb313e..a6f1f93 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -145,7 +145,8 @@
       @Override
       protected void configure() {
         bind(DataSourceType.class).toInstance(dst);
-      }});
+      }
+    });
     modules.add(new DatabaseModule());
     modules.add(new SchemaModule());
     modules.add(new LocalDiskRepositoryManager.Module());
diff --git a/gerrit-prettify/BUCK b/gerrit-prettify/BUCK
index 6728ba6..9a0136e 100644
--- a/gerrit-prettify/BUCK
+++ b/gerrit-prettify/BUCK
@@ -15,6 +15,7 @@
     '//gerrit-gwtexpui:SafeHtml',
   ],
   exported_deps = [
+    '//gerrit-extension-api:client',
     '//gerrit-patch-jgit:client',
     '//gerrit-reviewdb:client',
     '//lib:gwtjsonrpc',
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
index cdf800c..49dc2fc 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.prettify.client;
 
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
@@ -73,7 +73,7 @@
   protected SparseFileContent content;
   protected EditFilter side;
   protected List<Edit> edits;
-  protected AccountDiffPreference diffPrefs;
+  protected DiffPreferencesInfo diffPrefs;
   protected String fileName;
   protected Set<Integer> trailingEdits;
 
@@ -110,7 +110,7 @@
     edits = all;
   }
 
-  public void setDiffPrefs(AccountDiffPreference how) {
+  public void setDiffPrefs(DiffPreferencesInfo how) {
     diffPrefs = how;
   }
 
@@ -132,7 +132,7 @@
     String html = toHTML(src);
 
     html = expandTabs(html);
-    if (diffPrefs.isSyntaxHighlighting() && getFileType() != null
+    if (diffPrefs.syntaxHighlighting && getFileType() != null
         && src.isWholeFile()) {
       // The prettify parsers don't like &#39; as an entity for the
       // single quote character. Replace them all out so we don't
@@ -233,7 +233,7 @@
       cleanText(txt, pos, start);
       pos = txt.indexOf(';', start + 1) + 1;
 
-      if (diffPrefs.getLineLength() <= col) {
+      if (diffPrefs.lineLength <= col) {
         buf.append("<br />");
         col = 0;
       }
@@ -247,14 +247,14 @@
 
   private void cleanText(String txt, int pos, int end) {
     while (pos < end) {
-      int free = diffPrefs.getLineLength() - col;
+      int free = diffPrefs.lineLength - col;
       if (free <= 0) {
         // The current line is full. Throw an explicit line break
         // onto the end, and we'll continue on the next line.
         //
         buf.append("<br />");
         col = 0;
-        free = diffPrefs.getLineLength();
+        free = diffPrefs.lineLength;
       }
 
       int n = Math.min(end - pos, free);
@@ -326,7 +326,7 @@
   private String toHTML(SparseFileContent src) {
     SafeHtml html;
 
-    if (diffPrefs.isIntralineDifference()) {
+    if (diffPrefs.intralineDifference) {
       html = colorLineEdits(src);
     } else {
       SafeHtmlBuilder b = new SafeHtmlBuilder();
@@ -342,7 +342,7 @@
       html = html.replaceAll("\r([^\n])", r);
     }
 
-    if (diffPrefs.isShowWhitespaceErrors()) {
+    if (diffPrefs.showWhitespaceErrors) {
       // We need to do whitespace errors before showing tabs, because
       // these patterns rely on \t as a literal, before it expands.
       //
@@ -350,12 +350,12 @@
       html = showTrailingWhitespace(html);
     }
 
-    if (diffPrefs.isShowLineEndings()){
+    if (diffPrefs.showLineEndings){
       html = showLineEndings(html);
     }
 
-    if (diffPrefs.isShowTabs()) {
-      String t = 1 < diffPrefs.getTabSize() ? "\t" : "";
+    if (diffPrefs.showTabs) {
+      String t = 1 < diffPrefs.tabSize ? "\t" : "";
       html = html.replaceAll("\t", "<span class=\"vt\">\u00BB</span>" + t);
     }
 
@@ -528,10 +528,10 @@
   private String expandTabs(String html) {
     StringBuilder tmp = new StringBuilder();
     int i = 0;
-    if (diffPrefs.isShowTabs()) {
+    if (diffPrefs.showTabs) {
       i = 1;
     }
-    for (; i < diffPrefs.getTabSize(); i++) {
+    for (; i < diffPrefs.tabSize; i++) {
       tmp.append("&nbsp;");
     }
     return html.replaceAll("\t", tmp.toString());
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
index 3e18bc8..295239f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.reviewdb.client.RefNames.REFS_USERS;
 
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
 
@@ -51,7 +52,7 @@
  * notifications of updates on that change, or just book-marking it for faster
  * future reference. One record per starred change.</li>
  *
- * <li>{@link AccountDiffPreference}: user's preferences for rendering side-to-side
+ * <li>{@link DiffPreferencesInfo}: user's preferences for rendering side-to-side
  * and unified diff</li>
  *
  * </ul>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
index 7948080..cc0cbf0 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
@@ -32,9 +32,9 @@
 
   public static enum Whitespace implements CodedEnum {
     IGNORE_NONE('N'), //
-    IGNORE_SPACE_AT_EOL('E'), //
-    IGNORE_SPACE_CHANGE('S'), //
-    IGNORE_ALL_SPACE('A');
+    IGNORE_TRAILING('E'), //
+    IGNORE_LEADING_AND_TRAILING('S'), //
+    IGNORE_ALL('A');
 
     private final char code;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 26d31da..d2906d3 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -39,8 +39,6 @@
  *          |
  *          +- {@link PatchSetApproval}: a +/- vote on the change's current state.
  *          |
- *          +- {@link PatchSetAncestor}: parents of this change's commit.
- *          |
  *          +- {@link PatchLineComment}: comment about a specific line
  * </pre>
  * <p>
@@ -51,11 +49,6 @@
  * {@link Account} is usually also listed as the author and committer in the
  * PatchSetInfo.
  * <p>
- * The {@link PatchSetAncestor} entities are a mirror of the Git commit
- * metadata, providing access to the information without needing direct
- * accessing Git. These entities are actually legacy artifacts from Gerrit 1.x
- * and could be removed, replaced by direct RevCommit access.
- * <p>
  * Each PatchSet contains zero or more Patch records, detailing the file paths
  * impacted by the change (otherwise known as, the file paths the author
  * added/deleted/modified). Sometimes a merge commit can contain zero patches,
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
index acf8b45..3ecd539 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
@@ -52,7 +52,7 @@
     }
 
     @Override
-    protected void set(String newValue) {
+    public void set(String newValue) {
       uuid = newValue;
     }
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetAncestor.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetAncestor.java
deleted file mode 100644
index 8412788..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetAncestor.java
+++ /dev/null
@@ -1,88 +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.gwtorm.client.Column;
-import com.google.gwtorm.client.IntKey;
-
-/** Ancestors of a {@link PatchSet} that the PatchSet depends upon. */
-public final class PatchSetAncestor {
-  public static class Id extends IntKey<PatchSet.Id> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1, name = Column.NONE)
-    protected PatchSet.Id patchSetId;
-
-    @Column(id = 2)
-    protected int position;
-
-    protected Id() {
-      patchSetId = new PatchSet.Id();
-    }
-
-    public Id(final PatchSet.Id psId, final int pos) {
-      this.patchSetId = psId;
-      this.position = pos;
-    }
-
-    @Override
-    public PatchSet.Id getParentKey() {
-      return patchSetId;
-    }
-
-    @Override
-    public int get() {
-      return position;
-    }
-
-    @Override
-    protected void set(int newValue) {
-      position = newValue;
-    }
-  }
-
-  @Column(id = 1, name = Column.NONE)
-  protected Id key;
-
-  @Column(id = 2)
-  protected RevId ancestorRevision;
-
-  protected PatchSetAncestor() {
-  }
-
-  public PatchSetAncestor(final PatchSetAncestor.Id k) {
-    key = k;
-  }
-
-  public PatchSetAncestor.Id getId() {
-    return key;
-  }
-
-  public PatchSet.Id getPatchSet() {
-    return key.patchSetId;
-  }
-
-  public int getPosition() {
-    return key.position;
-  }
-
-  public RevId getAncestorRevision() {
-    return ancestorRevision;
-  }
-
-  public void setAncestorRevision(final RevId id) {
-    ancestorRevision = id;
-  }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
index ce1b27f..af9e75c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
@@ -97,6 +97,7 @@
   protected InheritableBoolean createNewChangeForAllNotInTarget;
 
   protected InheritableBoolean enableSignedPush;
+  protected InheritableBoolean requireSignedPush;
 
   protected Project() {
   }
@@ -111,6 +112,7 @@
     useContentMerge = InheritableBoolean.INHERIT;
     createNewChangeForAllNotInTarget = InheritableBoolean.INHERIT;
     enableSignedPush = InheritableBoolean.INHERIT;
+    requireSignedPush = InheritableBoolean.INHERIT;
   }
 
   public Project.NameKey getNameKey() {
@@ -182,6 +184,14 @@
     enableSignedPush = enable;
   }
 
+  public InheritableBoolean getRequireSignedPush() {
+    return requireSignedPush;
+  }
+
+  public void setRequireSignedPush(InheritableBoolean require) {
+    requireSignedPush = require;
+  }
+
   public void setMaxObjectSizeLimit(final String limit) {
     maxObjectSizeLimit = limit;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
deleted file mode 100644
index 02459d9..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
+++ /dev/null
@@ -1,45 +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.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
-import com.google.gerrit.reviewdb.client.RevId;
-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 PatchSetAncestorAccess extends
-    Access<PatchSetAncestor, PatchSetAncestor.Id> {
-  @Override
-  @PrimaryKey("key")
-  PatchSetAncestor get(PatchSetAncestor.Id key) throws OrmException;
-
-  @Query("WHERE key.patchSetId = ? ORDER BY key.position")
-  ResultSet<PatchSetAncestor> ancestorsOf(PatchSet.Id id) throws OrmException;
-
-  @Query("WHERE key.patchSetId.changeId = ?")
-  ResultSet<PatchSetAncestor> byChange(Change.Id id) throws OrmException;
-
-  @Query("WHERE key.patchSetId = ?")
-  ResultSet<PatchSetAncestor> byPatchSet(PatchSet.Id id) throws OrmException;
-
-  @Query("WHERE ancestorRevision = ?")
-  ResultSet<PatchSetAncestor> descendantsOf(RevId revision)
-      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 85f2b26..37dcc59 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
@@ -92,8 +92,7 @@
   @Relation(id = 24)
   PatchSetAccess patchSets();
 
-  @Relation(id = 25)
-  PatchSetAncestorAccess patchSetAncestors();
+  // Deleted @Relation(id = 25)
 
   @Relation(id = 26)
   PatchLineCommentAccess patchComments();
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 a62c762..1162a5f 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
@@ -88,14 +88,6 @@
 ON patch_sets (revision);
 
 -- *********************************************************************
--- PatchSetAncestorAccess
---    @PrimaryKey covers: ancestorsOf
---    covers:             descendantsOf
-CREATE INDEX patch_set_ancestors_desc
-ON patch_set_ancestors (ancestor_revision);
-
-
--- *********************************************************************
 -- StarredChangeAccess
 --    @PrimaryKey covers: byAccount
 
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 f88c169..258b7be 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
@@ -97,15 +97,6 @@
 #
 
 -- *********************************************************************
--- PatchSetAncestorAccess
---    @PrimaryKey covers: ancestorsOf
---    covers:             descendantsOf
-CREATE INDEX patch_set_ancestors_desc
-ON patch_set_ancestors (ancestor_revision)
-#
-
-
--- *********************************************************************
 -- StarredChangeAccess
 --    @PrimaryKey covers: byAccount
 
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 a6b21ee..1fe7dce 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
@@ -137,13 +137,6 @@
 ON patch_sets (revision);
 
 -- *********************************************************************
--- PatchSetAncestorAccess
---    @PrimaryKey covers: ancestorsOf
---    covers:             descendantsOf
-CREATE INDEX patch_set_ancestors_desc
-ON patch_set_ancestors (ancestor_revision);
-
--- *********************************************************************
 -- StarredChangeAccess
 --    @PrimaryKey covers: byAccount
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
index 285f1d4..889f008 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.data.ApprovalAttribute;
+import com.google.gerrit.server.data.PatchSetAttribute;
 import com.google.gerrit.server.events.ChangeAbandonedEvent;
 import com.google.gerrit.server.events.ChangeMergedEvent;
 import com.google.gerrit.server.events.ChangeRestoredEvent;
@@ -62,6 +63,7 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -334,6 +336,16 @@
       }
     }
 
+    private PatchSetAttribute asPatchSetAttribute(Change change,
+        PatchSet patchSet, ReviewDb db) throws OrmException {
+      try (Repository repo = repoManager.openRepository(change.getProject());
+          RevWalk revWalk = new RevWalk(repo)) {
+        return eventFactory.asPatchSetAttribute(db, revWalk, patchSet);
+      } catch (IOException e) {
+        throw new OrmException(e);
+      }
+    }
+
     /**
      * Fire the update hook
      *
@@ -380,8 +392,8 @@
       AccountState uploader = accountCache.get(patchSet.getUploader());
       AccountState owner = accountCache.get(change.getOwner());
 
-      event.change = eventFactory.asChangeAttribute(change);
-      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      event.change = eventFactory.asChangeAttribute(db, change);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
       event.uploader = eventFactory.asAccountAttribute(uploader.getAccount());
       fireEvent(change, event, db);
 
@@ -408,8 +420,8 @@
       AccountState uploader = accountCache.get(patchSet.getUploader());
       AccountState owner = accountCache.get(change.getOwner());
 
-      event.change = eventFactory.asChangeAttribute(change);
-      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      event.change = eventFactory.asChangeAttribute(db, change);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
       event.uploader = eventFactory.asAccountAttribute(uploader.getAccount());
       fireEvent(change, event, db);
 
@@ -434,9 +446,9 @@
       CommentAddedEvent event = new CommentAddedEvent();
       AccountState owner = accountCache.get(change.getOwner());
 
-      event.change = eventFactory.asChangeAttribute(change);
+      event.change = eventFactory.asChangeAttribute(db, change);
       event.author =  eventFactory.asAccountAttribute(account);
-      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
       event.comment = comment;
 
       LabelTypes labelTypes = projectCache.get(change.getProject()).getLabelTypes();
@@ -478,9 +490,9 @@
       ChangeMergedEvent event = new ChangeMergedEvent();
       AccountState owner = accountCache.get(change.getOwner());
 
-      event.change = eventFactory.asChangeAttribute(change);
+      event.change = eventFactory.asChangeAttribute(db, change);
       event.submitter = eventFactory.asAccountAttribute(account);
-      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
       event.newRev = mergeResultRev;
       fireEvent(change, event, db);
 
@@ -505,9 +517,9 @@
       MergeFailedEvent event = new MergeFailedEvent();
       AccountState owner = accountCache.get(change.getOwner());
 
-      event.change = eventFactory.asChangeAttribute(change);
+      event.change = eventFactory.asChangeAttribute(db, change);
       event.submitter = eventFactory.asAccountAttribute(account);
-      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
       event.reason = reason;
       fireEvent(change, event, db);
 
@@ -532,9 +544,9 @@
       ChangeAbandonedEvent event = new ChangeAbandonedEvent();
       AccountState owner = accountCache.get(change.getOwner());
 
-      event.change = eventFactory.asChangeAttribute(change);
+      event.change = eventFactory.asChangeAttribute(db, change);
       event.abandoner = eventFactory.asAccountAttribute(account);
-      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
       event.reason = reason;
       fireEvent(change, event, db);
 
@@ -559,9 +571,9 @@
       ChangeRestoredEvent event = new ChangeRestoredEvent();
       AccountState owner = accountCache.get(change.getOwner());
 
-      event.change = eventFactory.asChangeAttribute(change);
+      event.change = eventFactory.asChangeAttribute(db, change);
       event.restorer = eventFactory.asAccountAttribute(account);
-      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
       event.reason = reason;
       fireEvent(change, event, db);
 
@@ -615,8 +627,8 @@
       ReviewerAddedEvent event = new ReviewerAddedEvent();
       AccountState owner = accountCache.get(change.getOwner());
 
-      event.change = eventFactory.asChangeAttribute(change);
-      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      event.change = eventFactory.asChangeAttribute(db, change);
+      event.patchSet = asPatchSetAttribute(change, patchSet, db);
       event.reviewer = eventFactory.asAccountAttribute(account);
       fireEvent(change, event, db);
 
@@ -638,7 +650,7 @@
       TopicChangedEvent event = new TopicChangedEvent();
       AccountState owner = accountCache.get(change.getOwner());
 
-      event.change = eventFactory.asChangeAttribute(change);
+      event.change = eventFactory.asChangeAttribute(db, change);
       event.changer = eventFactory.asAccountAttribute(account);
       event.oldTopic = oldTopic;
       fireEvent(change, event, db);
@@ -670,7 +682,7 @@
       HashtagsChangedEvent event = new HashtagsChangedEvent();
       AccountState owner = accountCache.get(change.getOwner());
 
-      event.change = eventFactory.asChangeAttribute(change);
+      event.change = eventFactory.asChangeAttribute(db, change);
       event.editor = eventFactory.asAccountAttribute(account);
       event.hashtags = hashtagArray(hashtags);
       event.added = hashtagArray(added);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index 961cc45..44c1f01 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -17,8 +17,8 @@
 import static com.google.gerrit.rules.StoredValue.create;
 
 import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
index 9a64369..1d1f571 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalCopier.java
@@ -46,6 +46,7 @@
 
 import java.io.IOException;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.NavigableSet;
 import java.util.Objects;
@@ -87,7 +88,11 @@
 
   Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
       ChangeControl ctl, PatchSet.Id psId) throws OrmException {
-    return getForPatchSet(db, ctl, db.patchSets().get(psId));
+    PatchSet ps = db.patchSets().get(psId);
+    if (ps == null) {
+      return Collections.emptyList();
+    }
+    return getForPatchSet(db, ctl, ps);
   }
 
   private Iterable<PatchSetApproval> getForPatchSet(ReviewDb db,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 0de7e38..6c80d26 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -21,13 +21,13 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Ordering;
 import com.google.gerrit.common.TimeUtil;
+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.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.change.ChangeMessages;
@@ -39,6 +39,7 @@
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.mail.RevertedSender;
+import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.RefControl;
@@ -72,7 +73,6 @@
 
 import java.io.IOException;
 import java.text.MessageFormat;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -149,19 +149,6 @@
     c.setLastUpdatedOn(TimeUtil.nowTs());
   }
 
-  public static void insertAncestors(ReviewDb db, PatchSet.Id id, RevCommit src)
-      throws OrmException {
-    int cnt = src.getParentCount();
-    List<PatchSetAncestor> toInsert = new ArrayList<>(cnt);
-    for (int p = 0; p < cnt; p++) {
-      PatchSetAncestor a =
-          new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1));
-      a.setAncestorRevision(new RevId(src.getParent(p).getId().getName()));
-      toInsert.add(a);
-    }
-    db.patchSetAncestors().insert(toInsert);
-  }
-
   public static PatchSet.Id nextPatchSetId(Map<String, Ref> allRefs,
       PatchSet.Id id) {
     PatchSet.Id next = nextPatchSetId(id);
@@ -199,6 +186,8 @@
   private final GitReferenceUpdated gitRefUpdated;
   private final ChangeIndexer indexer;
   private final BatchUpdate.Factory updateFactory;
+  private final ChangeMessagesUtil changeMessagesUtil;
+  private final ChangeUpdate.Factory changeUpdateFactory;
 
   @Inject
   ChangeUtil(Provider<IdentifiedUser> user,
@@ -209,7 +198,9 @@
       GitRepositoryManager gitManager,
       GitReferenceUpdated gitRefUpdated,
       ChangeIndexer indexer,
-      BatchUpdate.Factory updateFactory) {
+      BatchUpdate.Factory updateFactory,
+      ChangeMessagesUtil changeMessagesUtil,
+      ChangeUpdate.Factory changeUpdateFactory) {
     this.user = user;
     this.db = db;
     this.queryProvider = queryProvider;
@@ -219,6 +210,8 @@
     this.gitRefUpdated = gitRefUpdated;
     this.indexer = indexer;
     this.updateFactory = updateFactory;
+    this.changeMessagesUtil = changeMessagesUtil;
+    this.changeUpdateFactory = changeUpdateFactory;
   }
 
   public Change.Id revert(ChangeControl ctl, PatchSet.Id patchSetId,
@@ -242,6 +235,10 @@
       PersonIdent authorIdent = user.get()
           .newCommitterIdent(myIdent.getWhen(), myIdent.getTimeZone());
 
+      if (commitToRevert.getParentCount() == 0) {
+        throw new ResourceConflictException("Cannot revert initial commit");
+      }
+
       RevCommit parentToCommitToRevert = commitToRevert.getParent(0);
       revWalk.parseHeaders(parentToCommitToRevert);
 
@@ -281,12 +278,22 @@
         ins = changeInserterFactory.create(
               refControl, change, revertCommit)
             .setValidatePolicy(CommitValidators.Policy.GERRIT);
+
+        ChangeMessage changeMessage = new ChangeMessage(
+            new ChangeMessage.Key(
+                patchSetId.getParentKey(), ChangeUtil.messageUUID(db.get())),
+                user.get().getAccountId(), TimeUtil.nowTs(), patchSetId);
         StringBuilder msgBuf = new StringBuilder();
         msgBuf.append("Patch Set ").append(patchSetId.get()).append(": Reverted");
         msgBuf.append("\n\n");
         msgBuf.append("This patchset was reverted in change: ")
               .append(change.getKey().get());
-        ins.setMessage(msgBuf.toString());
+        changeMessage.setMessage(msgBuf.toString());
+        ChangeUpdate update = changeUpdateFactory.create(ctl, TimeUtil.nowTs());
+        changeMessagesUtil.addChangeMessage(db.get(), update, changeMessage);
+        update.commit();
+
+        ins.setMessage("Uploaded patch set 1.");
         try (BatchUpdate bu = updateFactory.create(
             db.get(), change.getProject(), refControl.getUser(),
             change.getCreatedOn())) {
@@ -354,7 +361,6 @@
       db.patchComments().delete(db.patchComments().byChange(changeId));
 
       db.patchSetApprovals().delete(db.patchSetApprovals().byChange(changeId));
-      db.patchSetAncestors().delete(db.patchSetAncestors().byChange(changeId));
       db.patchSets().delete(patchSets);
       db.changeMessages().delete(db.changeMessages().byChange(changeId));
       db.starredChanges().delete(db.starredChanges().byChange(changeId));
@@ -461,7 +467,6 @@
     // No need to delete from notedb; draft patch sets will be filtered out.
     db.patchComments().delete(db.patchComments().byPatchSet(patchSetId));
     db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(patchSetId));
-    db.patchSetAncestors().delete(db.patchSetAncestors().byPatchSet(patchSetId));
 
     db.patchSets().delete(Collections.singleton(patch));
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index df25042..9e5ff12 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.StarredChange;
@@ -110,14 +109,16 @@
     public IdentifiedUser create(SocketAddress remotePeer, Account.Id id) {
       return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
           anonymousCowardName, canonicalUrl, accountCache, groupBackend,
-          disableReverseDnsLookup, Providers.of(remotePeer), null, id, null);
+          disableReverseDnsLookup, Providers.of(remotePeer), null,
+          id, null);
     }
 
     public CurrentUser runAs(SocketAddress remotePeer, Account.Id id,
         @Nullable CurrentUser caller) {
       return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
           anonymousCowardName, canonicalUrl, accountCache, groupBackend,
-          disableReverseDnsLookup, Providers.of(remotePeer), null, id, caller);
+          disableReverseDnsLookup, Providers.of(remotePeer), null,
+          id, caller);
     }
   }
 
@@ -151,7 +152,6 @@
         final AccountCache accountCache,
         final GroupBackend groupBackend,
         @DisableReverseDnsLookup final Boolean disableReverseDnsLookup,
-
         @RemotePeer final Provider<SocketAddress> remotePeerProvider,
         final Provider<ReviewDb> dbProvider) {
       this.capabilityControlFactory = capabilityControlFactory;
@@ -162,7 +162,6 @@
       this.accountCache = accountCache;
       this.groupBackend = groupBackend;
       this.disableReverseDnsLookup = disableReverseDnsLookup;
-
       this.remotePeerProvider = remotePeerProvider;
       this.dbProvider = dbProvider;
     }
@@ -170,13 +169,15 @@
     public IdentifiedUser create(Account.Id id) {
       return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
           anonymousCowardName, canonicalUrl, accountCache, groupBackend,
-          disableReverseDnsLookup, remotePeerProvider, dbProvider, id, null);
+          disableReverseDnsLookup, remotePeerProvider, dbProvider,
+          id, null);
     }
 
     public IdentifiedUser runAs(Account.Id id, CurrentUser caller) {
       return new IdentifiedUser(capabilityControlFactory, authConfig, realm,
           anonymousCowardName, canonicalUrl, accountCache, groupBackend,
-          disableReverseDnsLookup, remotePeerProvider, dbProvider, id, caller);
+          disableReverseDnsLookup, remotePeerProvider, dbProvider,
+          id, caller);
     }
   }
 
@@ -274,20 +275,6 @@
     return state().getAccount();
   }
 
-  public AccountDiffPreference getAccountDiffPreference() {
-    AccountDiffPreference diffPref;
-    try {
-      diffPref = dbProvider.get().accountDiffPreferences().get(getAccountId());
-      if (diffPref == null) {
-        diffPref = AccountDiffPreference.createDefault(getAccountId());
-      }
-    } catch (OrmException e) {
-      log.warn("Cannot query account diff preferences", e);
-      diffPref = AccountDiffPreference.createDefault(getAccountId());
-    }
-    return diffPref;
-  }
-
   public boolean hasEmailAddress(String email) {
     if (validEmails.contains(email)) {
       return true;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
index 19aeefc..d2d6bf2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetDiffPreferences.java
@@ -14,91 +14,140 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.extensions.client.Theme;
+import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.UserConfigSections;
+import com.google.gerrit.server.patch.PatchListKey;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
 @Singleton
 public class GetDiffPreferences implements RestReadView<AccountResource> {
   private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> db;
+  private final Provider<AllUsersName> allUsersName;
+  private final GitRepositoryManager gitMgr;
+  private final boolean readFromGit;
 
   @Inject
-  GetDiffPreferences(Provider<CurrentUser> self, Provider<ReviewDb> db) {
+  GetDiffPreferences(Provider<CurrentUser> self,
+      Provider<ReviewDb> db,
+      @GerritServerConfig Config cfg,
+      Provider<AllUsersName> allUsersName,
+      GitRepositoryManager gitMgr) {
     this.self = self;
     this.db = db;
+    this.allUsersName = allUsersName;
+    this.gitMgr = gitMgr;
+    readFromGit = cfg.getBoolean("user", null, "readPrefsFromGit", false);
   }
 
   @Override
   public DiffPreferencesInfo apply(AccountResource rsrc)
-      throws AuthException, OrmException {
+      throws AuthException, OrmException, ConfigInvalidException, IOException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException("restricted to administrator");
     }
 
     Account.Id userId = rsrc.getUser().getAccountId();
-    AccountDiffPreference a = db.get().accountDiffPreferences().get(userId);
-    if (a == null) {
-      a = new AccountDiffPreference(userId);
-    }
-    return DiffPreferencesInfo.parse(a);
+    return readFromGit
+        ? readFromGit(userId, gitMgr, allUsersName.get(), null)
+        : readFromDb(userId);
   }
 
-  public static class DiffPreferencesInfo {
-    static DiffPreferencesInfo parse(AccountDiffPreference p) {
-      DiffPreferencesInfo info = new DiffPreferencesInfo();
-      info.context = p.getContext();
-      info.expandAllComments = p.isExpandAllComments() ? true : null;
-      info.ignoreWhitespace = p.getIgnoreWhitespace();
-      info.intralineDifference = p.isIntralineDifference() ? true : null;
-      info.lineLength = p.getLineLength();
-      info.manualReview = p.isManualReview() ? true : null;
-      info.retainHeader = p.isRetainHeader() ? true : null;
-      info.showLineEndings = p.isShowLineEndings() ? true : null;
-      info.showTabs = p.isShowTabs() ? true : null;
-      info.showWhitespaceErrors = p.isShowWhitespaceErrors() ? true : null;
-      info.skipDeleted = p.isSkipDeleted() ? true : null;
-      info.skipUncommented = p.isSkipUncommented() ? true : null;
-      info.hideTopMenu = p.isHideTopMenu() ? true : null;
-      info.autoHideDiffTableHeader = p.isAutoHideDiffTableHeader() ? true : null;
-      info.hideLineNumbers = p.isHideLineNumbers() ? true : null;
-      info.syntaxHighlighting = p.isSyntaxHighlighting() ? true : null;
-      info.tabSize = p.getTabSize();
-      info.renderEntireFile = p.isRenderEntireFile() ? true : null;
-      info.hideEmptyPane = p.isHideEmptyPane() ? true : null;
-      info.theme = p.getTheme();
-      return info;
+  static DiffPreferencesInfo readFromGit(Account.Id id,
+      GitRepositoryManager gitMgr, AllUsersName allUsersName,
+      DiffPreferencesInfo in)
+      throws IOException, ConfigInvalidException, RepositoryNotFoundException {
+    try (Repository git = gitMgr.openRepository(allUsersName)) {
+      VersionedAccountPreferences p =
+          VersionedAccountPreferences.forUser(id);
+      p.load(git);
+      DiffPreferencesInfo prefs = new DiffPreferencesInfo();
+      loadSection(p.getConfig(), UserConfigSections.DIFF, null, prefs,
+          DiffPreferencesInfo.defaults(), in);
+      return prefs;
+    }
+  }
+
+  private DiffPreferencesInfo readFromDb(Account.Id id)
+      throws OrmException {
+    AccountDiffPreference a = db.get().accountDiffPreferences().get(id);
+    return nullify(initFromDb(a));
+  }
+
+  static DiffPreferencesInfo initFromDb(AccountDiffPreference a) {
+    DiffPreferencesInfo prefs = DiffPreferencesInfo.defaults();
+    if (a != null) {
+      prefs.context = (int)a.getContext();
+      prefs.expandAllComments = a.isExpandAllComments();
+      prefs.hideLineNumbers = a.isHideLineNumbers();
+      prefs.hideTopMenu = a.isHideTopMenu();
+      prefs.ignoreWhitespace = PatchListKey.WHITESPACE_TYPES.inverse().get(
+          a.getIgnoreWhitespace().getCode());
+      prefs.intralineDifference = a.isIntralineDifference();
+      prefs.lineLength = a.getLineLength();
+      prefs.manualReview = a.isManualReview();
+      prefs.renderEntireFile = a.isRenderEntireFile();
+      prefs.retainHeader = a.isRetainHeader();
+      prefs.showLineEndings = a.isShowLineEndings();
+      prefs.showTabs = a.isShowTabs();
+      prefs.showWhitespaceErrors = a.isShowWhitespaceErrors();
+      prefs.skipDeleted = a.isSkipDeleted();
+      prefs.skipUncommented = a.isSkipUncommented();
+      prefs.syntaxHighlighting = a.isSyntaxHighlighting();
+      prefs.tabSize = a.getTabSize();
+      prefs.theme = a.getTheme();
+      prefs.hideEmptyPane = a.isHideEmptyPane();
+      prefs.autoHideDiffTableHeader = a.isAutoHideDiffTableHeader();
     }
 
-    public short context;
-    public Boolean expandAllComments;
-    public Whitespace ignoreWhitespace;
-    public Boolean intralineDifference;
-    public int lineLength;
-    public Boolean manualReview;
-    public Boolean retainHeader;
-    public Boolean showLineEndings;
-    public Boolean showTabs;
-    public Boolean showWhitespaceErrors;
-    public Boolean skipDeleted;
-    public Boolean skipUncommented;
-    public Boolean syntaxHighlighting;
-    public Boolean hideTopMenu;
-    public Boolean autoHideDiffTableHeader;
-    public Boolean hideLineNumbers;
-    public Boolean renderEntireFile;
-    public Boolean hideEmptyPane;
-    public int tabSize;
-    public Theme theme;
+    return prefs;
+  }
+
+  private static DiffPreferencesInfo nullify(DiffPreferencesInfo prefs) {
+    prefs.expandAllComments = b(prefs.expandAllComments);
+    prefs.hideLineNumbers = b(prefs.hideLineNumbers);
+    prefs.hideTopMenu = b(prefs.hideTopMenu);
+    prefs.intralineDifference = b(prefs.intralineDifference);
+    prefs.manualReview = b(prefs.manualReview);
+    prefs.renderEntireFile = b(prefs.renderEntireFile);
+    prefs.retainHeader = b(prefs.retainHeader);
+    prefs.showLineEndings = b(prefs.showLineEndings);
+    prefs.showTabs = b(prefs.showTabs);
+    prefs.showWhitespaceErrors = b(prefs.showWhitespaceErrors);
+    prefs.skipDeleted = b(prefs.skipDeleted);
+    prefs.skipUncommented = b(prefs.skipUncommented);
+    prefs.syntaxHighlighting = b(prefs.syntaxHighlighting);
+    prefs.hideEmptyPane = b(prefs.hideEmptyPane);
+    prefs.autoHideDiffTableHeader = b(prefs.autoHideDiffTableHeader);
+    return prefs;
+  }
+
+  private static Boolean b(Boolean b) {
+    if (b == null) {
+      return null;
+    }
+    return b ? Boolean.TRUE : null;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java
index e6e7644..d99b68f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetEditPreferences.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -28,6 +29,7 @@
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
@@ -55,13 +57,21 @@
       throw new AuthException("restricted to members of Modify Accounts");
     }
 
+    return readFromGit(
+        rsrc.getUser().getAccountId(), gitMgr, allUsersName, null);
+  }
+
+  static EditPreferencesInfo readFromGit(Account.Id id,
+      GitRepositoryManager gitMgr, AllUsersName allUsersName,
+      EditPreferencesInfo in) throws IOException, ConfigInvalidException,
+          RepositoryNotFoundException {
     try (Repository git = gitMgr.openRepository(allUsersName)) {
       VersionedAccountPreferences p =
-          VersionedAccountPreferences.forUser(rsrc.getUser().getAccountId());
+          VersionedAccountPreferences.forUser(id);
       p.load(git);
 
       return loadSection(p.getConfig(), UserConfigSections.EDIT, null,
-          new EditPreferencesInfo(), EditPreferencesInfo.defaults());
+          new EditPreferencesInfo(), EditPreferencesInfo.defaults(), in);
     }
   }
 }
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
index 4e08756..7b4d133 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetDiffPreferences.java
@@ -14,144 +14,224 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.extensions.client.Theme;
+import static com.google.gerrit.server.account.GetDiffPreferences.initFromDb;
+import static com.google.gerrit.server.account.GetDiffPreferences.readFromGit;
+import static com.google.gerrit.server.config.ConfigUtil.loadSection;
+import static com.google.gerrit.server.config.ConfigUtil.storeSection;
+
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.GetDiffPreferences.DiffPreferencesInfo;
-import com.google.gerrit.server.account.SetDiffPreferences.Input;
+import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.UserConfigSections;
+import com.google.gerrit.server.patch.PatchListKey;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
 import java.util.Collections;
 
 @Singleton
-public class SetDiffPreferences implements RestModifyView<AccountResource, Input> {
-  static class Input {
-    Short context;
-    Boolean expandAllComments;
-    Whitespace ignoreWhitespace;
-    Boolean intralineDifference;
-    Integer lineLength;
-    Boolean manualReview;
-    Boolean retainHeader;
-    Boolean showLineEndings;
-    Boolean showTabs;
-    Boolean showWhitespaceErrors;
-    Boolean skipDeleted;
-    Boolean skipUncommented;
-    Boolean syntaxHighlighting;
-    Boolean hideTopMenu;
-    Boolean autoHideDiffTableHeader;
-    Boolean hideLineNumbers;
-    Boolean renderEntireFile;
-    Integer tabSize;
-    Theme theme;
-    Boolean hideEmptyPane;
-  }
-
+public class SetDiffPreferences implements
+    RestModifyView<AccountResource, DiffPreferencesInfo> {
   private final Provider<CurrentUser> self;
   private final Provider<ReviewDb> db;
+  private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
+  private final AllUsersName allUsersName;
+  private final GitRepositoryManager gitMgr;
+  private final boolean readFromGit;
 
   @Inject
-  SetDiffPreferences(Provider<CurrentUser> self, Provider<ReviewDb> db) {
+  SetDiffPreferences(Provider<CurrentUser> self,
+      Provider<ReviewDb> db,
+      @GerritServerConfig Config cfg,
+      Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+      AllUsersName allUsersName,
+      GitRepositoryManager gitMgr) {
     this.self = self;
     this.db = db;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.allUsersName = allUsersName;
+    this.gitMgr = gitMgr;
+    readFromGit = cfg.getBoolean("user", null, "readPrefsFromGit", false);
   }
 
   @Override
-  public DiffPreferencesInfo apply(AccountResource rsrc, Input input)
-      throws AuthException, OrmException {
+  public DiffPreferencesInfo apply(AccountResource rsrc, DiffPreferencesInfo in)
+      throws AuthException, BadRequestException, ConfigInvalidException,
+      RepositoryNotFoundException, IOException, OrmException {
     if (self.get() != rsrc.getUser()
         && !self.get().getCapabilities().canModifyAccount()) {
       throw new AuthException("restricted to members of Modify Accounts");
     }
-    if (input == null) {
-      input = new Input();
+
+    if (in == null) {
+      throw new BadRequestException("input must be provided");
     }
 
-    Account.Id accountId = rsrc.getUser().getAccountId();
-    AccountDiffPreference p;
+    Account.Id userId = rsrc.getUser().getAccountId();
+    DiffPreferencesInfo n = readFromGit
+        ? readFromGit(userId, gitMgr, allUsersName, in)
+        : merge(initFromDb(db.get().accountDiffPreferences().get(userId)), in);
+    DiffPreferencesInfo out = writeToGit(n, userId);
+    writeToDb(n, userId);
+    return out;
+  }
 
-    db.get().accounts().beginTransaction(accountId);
+  private void writeToDb(DiffPreferencesInfo in, Account.Id id)
+      throws OrmException {
+    db.get().accounts().beginTransaction(id);
     try {
-      p = db.get().accountDiffPreferences().get(accountId);
-      if (p == null) {
-        p = new AccountDiffPreference(accountId);
-      }
-
-      if (input.context != null) {
-        p.setContext(input.context);
-      }
-      if (input.ignoreWhitespace != null) {
-        p.setIgnoreWhitespace(input.ignoreWhitespace);
-      }
-      if (input.expandAllComments != null) {
-        p.setExpandAllComments(input.expandAllComments);
-      }
-      if (input.intralineDifference != null) {
-        p.setIntralineDifference(input.intralineDifference);
-      }
-      if (input.lineLength != null) {
-        p.setLineLength(input.lineLength);
-      }
-      if (input.manualReview != null) {
-        p.setManualReview(input.manualReview);
-      }
-      if (input.retainHeader != null) {
-        p.setRetainHeader(input.retainHeader);
-      }
-      if (input.showLineEndings != null) {
-        p.setShowLineEndings(input.showLineEndings);
-      }
-      if (input.showTabs != null) {
-        p.setShowTabs(input.showTabs);
-      }
-      if (input.showWhitespaceErrors != null) {
-        p.setShowWhitespaceErrors(input.showWhitespaceErrors);
-      }
-      if (input.skipDeleted != null) {
-        p.setSkipDeleted(input.skipDeleted);
-      }
-      if (input.skipUncommented != null) {
-        p.setSkipUncommented(input.skipUncommented);
-      }
-      if (input.syntaxHighlighting != null) {
-        p.setSyntaxHighlighting(input.syntaxHighlighting);
-      }
-      if (input.hideTopMenu != null) {
-        p.setHideTopMenu(input.hideTopMenu);
-      }
-      if (input.autoHideDiffTableHeader != null) {
-        p.setAutoHideDiffTableHeader(input.autoHideDiffTableHeader);
-      }
-      if (input.hideLineNumbers != null) {
-        p.setHideLineNumbers(input.hideLineNumbers);
-      }
-      if (input.renderEntireFile != null) {
-        p.setRenderEntireFile(input.renderEntireFile);
-      }
-      if (input.tabSize != null) {
-        p.setTabSize(input.tabSize);
-      }
-      if (input.theme != null) {
-        p.setTheme(input.theme);
-      }
-      if (input.hideEmptyPane != null) {
-        p.setHideEmptyPane(input.hideEmptyPane);
-      }
-
+      AccountDiffPreference p = db.get().accountDiffPreferences().get(id);
+      p = initAccountDiffPreferences(p, in, id);
       db.get().accountDiffPreferences().upsert(Collections.singleton(p));
       db.get().commit();
     } finally {
       db.get().rollback();
     }
-    return DiffPreferencesInfo.parse(p);
+  }
+
+  private DiffPreferencesInfo writeToGit(DiffPreferencesInfo in,
+      Account.Id useId) throws RepositoryNotFoundException, IOException,
+          ConfigInvalidException {
+    MetaDataUpdate md = metaDataUpdateFactory.get().create(allUsersName);
+
+    VersionedAccountPreferences prefs;
+    DiffPreferencesInfo out = new DiffPreferencesInfo();
+    try {
+      prefs = VersionedAccountPreferences.forUser(useId);
+      prefs.load(md);
+      storeSection(prefs.getConfig(), UserConfigSections.DIFF, null, in,
+          DiffPreferencesInfo.defaults());
+      prefs.commit(md);
+      loadSection(prefs.getConfig(), UserConfigSections.DIFF, null, out,
+          DiffPreferencesInfo.defaults(), null);
+    } finally {
+      md.close();
+    }
+    return out;
+  }
+
+  // TODO(davido): Remove manual merging in follow-up change
+  private DiffPreferencesInfo merge(DiffPreferencesInfo n,
+      DiffPreferencesInfo i) {
+    if (i.context != null) {
+      n.context = i.context;
+    }
+    if (i.expandAllComments != null) {
+      n.expandAllComments = i.expandAllComments;
+    }
+    if (i.hideLineNumbers != null) {
+      n.hideLineNumbers = i.hideLineNumbers;
+    }
+    if (i.hideTopMenu != null) {
+      n.hideTopMenu = i.hideTopMenu;
+    }
+    if (i.ignoreWhitespace != null) {
+      n.ignoreWhitespace = i.ignoreWhitespace;
+    }
+    if (i.intralineDifference != null) {
+      n.intralineDifference = i.intralineDifference;
+    }
+    if (i.lineLength != null) {
+      n.lineLength = i.lineLength;
+    }
+    if (i.manualReview != null) {
+      n.manualReview = i.manualReview;
+    }
+    if (i.renderEntireFile != null) {
+      n.renderEntireFile = i.renderEntireFile;
+    }
+    if (i.retainHeader != null) {
+      n.retainHeader = i.retainHeader;
+    }
+    if (i.showLineEndings != null) {
+      n.showLineEndings = i.showLineEndings;
+    }
+    if (i.showTabs != null) {
+      n.showTabs = i.showTabs;
+    }
+    if (i.showWhitespaceErrors != null) {
+      n.showWhitespaceErrors = i.showWhitespaceErrors;
+    }
+    if (i.skipDeleted != null) {
+      n.skipDeleted = i.skipDeleted;
+    }
+    if (i.skipUncommented != null) {
+      n.skipUncommented = i.skipUncommented;
+    }
+    if (i.syntaxHighlighting != null) {
+      n.syntaxHighlighting = i.syntaxHighlighting;
+    }
+    if (i.tabSize != null) {
+      n.tabSize = i.tabSize;
+    }
+    if (i.theme != null) {
+      n.theme = i.theme;
+    }
+    if (i.hideEmptyPane != null) {
+      n.hideEmptyPane = i.hideEmptyPane;
+    }
+    if (i.autoHideDiffTableHeader != null) {
+      n.autoHideDiffTableHeader = i.autoHideDiffTableHeader;
+    }
+    return n;
+  }
+
+  private static AccountDiffPreference initAccountDiffPreferences(
+      AccountDiffPreference a, DiffPreferencesInfo i, Account.Id id) {
+    if (a == null) {
+      a = AccountDiffPreference.createDefault(id);
+    }
+    int context = i.context == null
+        ? DiffPreferencesInfo.DEFAULT_CONTEXT
+        :  i.context;
+    a.setContext((short)context);
+    a.setExpandAllComments(b(i.expandAllComments));
+    a.setHideLineNumbers(b(i.hideLineNumbers));
+    a.setHideTopMenu(b(i.hideTopMenu));
+    a.setIgnoreWhitespace(i.ignoreWhitespace == null
+        ? Whitespace.IGNORE_NONE
+        : Whitespace.forCode(
+            PatchListKey.WHITESPACE_TYPES.get(i.ignoreWhitespace)));
+    a.setIntralineDifference(b(i.intralineDifference));
+    a.setLineLength(i.lineLength == null
+        ? DiffPreferencesInfo.DEFAULT_LINE_LENGTH
+        : i.lineLength);
+    a.setManualReview(b(i.manualReview));
+    a.setRenderEntireFile(b(i.renderEntireFile));
+    a.setRetainHeader(b(i.retainHeader));
+    a.setShowLineEndings(b(i.showLineEndings));
+    a.setShowTabs(b(i.showTabs));
+    a.setShowWhitespaceErrors(b(i.showWhitespaceErrors));
+    a.setSkipDeleted(b(i.skipDeleted));
+    a.setSkipUncommented(b(i.skipUncommented));
+    a.setSyntaxHighlighting(b(i.syntaxHighlighting));
+    a.setTabSize(i.tabSize == null
+        ? DiffPreferencesInfo.DEFAULT_TAB_SIZE
+        : i.tabSize);
+    a.setTheme(i.theme);
+    a.setHideEmptyPane(b(i.hideEmptyPane));
+    a.setAutoHideDiffTableHeader(b(i.autoHideDiffTableHeader));
+    return a;
+  }
+
+  private static boolean b(Boolean b) {
+    return b == null ? false : b;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java
index 2df1a77..eabe31d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/SetEditPreferences.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+import static com.google.gerrit.server.account.GetEditPreferences.readFromGit;
 import static com.google.gerrit.server.config.ConfigUtil.storeSection;
 
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
@@ -24,6 +25,7 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.UserConfigSections;
 import com.google.inject.Inject;
@@ -41,14 +43,17 @@
 
   private final Provider<CurrentUser> self;
   private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
+  private final GitRepositoryManager gitMgr;
   private final AllUsersName allUsersName;
 
   @Inject
   SetEditPreferences(Provider<CurrentUser> self,
       Provider<MetaDataUpdate.User> metaDataUpdateFactory,
+      GitRepositoryManager gitMgr,
       AllUsersName allUsersName) {
     this.self = self;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.gitMgr = gitMgr;
     this.allUsersName = allUsersName;
   }
 
@@ -72,7 +77,8 @@
     try {
       prefs = VersionedAccountPreferences.forUser(accountId);
       prefs.load(md);
-      storeSection(prefs.getConfig(), UserConfigSections.EDIT, null, in,
+      storeSection(prefs.getConfig(), UserConfigSections.EDIT, null,
+          readFromGit(accountId, gitMgr, allUsersName, in),
           EditPreferencesInfo.defaults());
       prefs.commit(md);
     } finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
index 344eda1..90d6aae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -245,7 +245,6 @@
     ReviewDb db = ctx.getDb();
     ChangeControl ctl = ctx.getChangeControl();
     ChangeUpdate update = ctx.getChangeUpdate();
-    ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
     if (patchSet.getGroups() == null) {
       patchSet.setGroups(GroupCollector.getDefaultGroups(patchSet));
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 7d126e3..733d7a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -118,7 +118,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Map;
@@ -785,14 +784,6 @@
       return Collections.emptyList();
     }
 
-    // chronological order
-    Collections.sort(messages, new Comparator<ChangeMessage>() {
-      @Override
-      public int compare(ChangeMessage a, ChangeMessage b) {
-        return a.getWrittenOn().compareTo(b.getWrittenOn());
-      }
-    });
-
     List<ChangeMessageInfo> result =
         Lists.newArrayListWithCapacity(messages.size());
     for (ChangeMessage message : messages) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
index b0f14af..82aaecb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPick.java
@@ -25,7 +25,7 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.UpdateException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
@@ -87,7 +87,7 @@
       return json.create(ChangeJson.NO_OPTIONS).format(cherryPickedChangeId);
     } catch (InvalidChangeOperationException e) {
       throw new BadRequestException(e.getMessage());
-    } catch (MergeException | NoSuchChangeException e) {
+    } catch (IntegrationException | NoSuchChangeException e) {
       throw new ResourceConflictException(e.getMessage());
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
index e559964..fc4893c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CherryPickChange.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -33,8 +34,7 @@
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeConflictException;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeIdenticalTreeException;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.UpdateException;
@@ -112,7 +112,7 @@
       final RefControl refControl) throws NoSuchChangeException,
       OrmException, MissingObjectException,
       IncorrectObjectTypeException, IOException,
-      InvalidChangeOperationException, MergeException, UpdateException,
+      InvalidChangeOperationException, IntegrationException, UpdateException,
       RestApiException {
 
     if (Strings.isNullOrEmpty(ref)) {
@@ -195,7 +195,7 @@
           return newChange.getId();
         }
       } catch (MergeIdenticalTreeException | MergeConflictException e) {
-        throw new MergeException("Cherry pick failed: " + e.getMessage());
+        throw new IntegrationException("Cherry pick failed: " + e.getMessage());
       }
     } catch (RepositoryNotFoundException e) {
       throw new NoSuchChangeException(change.getId(), e);
@@ -254,7 +254,8 @@
 
   private void addMessageToSourceChange(Change change, PatchSet.Id patchSetId,
       String destinationBranch, CodeReviewCommit cherryPickCommit,
-      IdentifiedUser identifiedUser, RefControl refControl) throws OrmException {
+      IdentifiedUser identifiedUser, RefControl refControl)
+          throws OrmException, IOException {
     ChangeMessage changeMessage = new ChangeMessage(
         new ChangeMessage.Key(
             patchSetId.getParentKey(), ChangeUtil.messageUUID(db.get())),
@@ -270,8 +271,9 @@
     changeMessage.setMessage(sb.toString());
 
     ChangeControl ctl = refControl.getProjectControl().controlFor(change);
-    ChangeUpdate update = updateFactory.create(ctl, change.getCreatedOn());
+    ChangeUpdate update = updateFactory.create(ctl, TimeUtil.nowTs());
     changeMessagesUtil.addChangeMessage(db.get(), update, changeMessage);
+    update.commit();
   }
 
   private String messageForDestinationChange(PatchSet.Id patchSetId,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 8a0a47e..ddeb5c0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -583,8 +583,6 @@
         // historical information.
         db.accountPatchReviews().delete(
             db.accountPatchReviews().byPatchSet(psId));
-        db.patchSetAncestors().delete(
-            db.patchSetAncestors().byPatchSet(psId));
         db.patchSetApprovals().delete(
             db.patchSetApprovals().byPatchSet(psId));
         db.patchComments().delete(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
index 84f8a04..71974c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/FileInfoJson.java
@@ -16,8 +16,8 @@
 
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.common.FileInfo;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
index eef0533..3dcc442 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDiff.java
@@ -24,6 +24,7 @@
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.common.ChangeType;
 import com.google.gerrit.extensions.common.DiffInfo;
 import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
@@ -39,8 +40,6 @@
 import com.google.gerrit.extensions.restapi.Response;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
@@ -52,7 +51,6 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
 
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.ReplaceEdit;
@@ -69,6 +67,8 @@
 import java.util.List;
 import java.util.concurrent.TimeUnit;
 
+import javax.inject.Inject;
+
 public class GetDiff implements RestReadView<FileResource> {
   private static final ImmutableMap<Patch.ChangeType, ChangeType> CHANGE_TYPE =
       Maps.immutableEnumMap(
@@ -93,7 +93,7 @@
   IgnoreWhitespace ignoreWhitespace = IgnoreWhitespace.NONE;
 
   @Option(name = "--context", handler = ContextOptionHandler.class)
-  short context = AccountDiffPreference.DEFAULT_CONTEXT;
+  int context = DiffPreferencesInfo.DEFAULT_CONTEXT;
 
   @Option(name = "--intraline")
   boolean intraline;
@@ -122,10 +122,10 @@
           resource.getRevision().getChangeResource(), IdString.fromDecoded(base));
       basePatchSet = baseResource.getPatchSet();
     }
-    AccountDiffPreference prefs = new AccountDiffPreference(new Account.Id(0));
-    prefs.setIgnoreWhitespace(ignoreWhitespace.whitespace);
-    prefs.setContext(context);
-    prefs.setIntralineDifference(intraline);
+    DiffPreferencesInfo prefs = new DiffPreferencesInfo();
+    prefs.ignoreWhitespace = ignoreWhitespace.whitespace;
+    prefs.context = context;
+    prefs.intralineDifference = intraline;
 
     try {
       PatchScriptFactory psf = patchScriptFactoryFactory.create(
@@ -135,7 +135,7 @@
           resource.getPatchKey().getParentKey(),
           prefs);
       psf.setLoadHistory(false);
-      psf.setLoadComments(context != AccountDiffPreference.WHOLE_FILE_CONTEXT);
+      psf.setLoadComments(context != DiffPreferencesInfo.WHOLE_FILE_CONTEXT);
       PatchScript ps = psf.call();
       Content content = new Content(ps);
       for (Edit edit : ps.getEdits()) {
@@ -369,14 +369,14 @@
   }
 
   enum IgnoreWhitespace {
-    NONE(AccountDiffPreference.Whitespace.IGNORE_NONE),
-    TRAILING(AccountDiffPreference.Whitespace.IGNORE_SPACE_AT_EOL),
-    CHANGED(AccountDiffPreference.Whitespace.IGNORE_SPACE_CHANGE),
-    ALL(AccountDiffPreference.Whitespace.IGNORE_ALL_SPACE);
+    NONE(DiffPreferencesInfo.Whitespace.IGNORE_NONE),
+    TRAILING(DiffPreferencesInfo.Whitespace.IGNORE_TRAILING),
+    CHANGED(DiffPreferencesInfo.Whitespace.IGNORE_LEADING_AND_TRAILING),
+    ALL(DiffPreferencesInfo.Whitespace.IGNORE_ALL);
 
-    private final AccountDiffPreference.Whitespace whitespace;
+    private final DiffPreferencesInfo.Whitespace whitespace;
 
-    private IgnoreWhitespace(AccountDiffPreference.Whitespace whitespace) {
+    private IgnoreWhitespace(DiffPreferencesInfo.Whitespace whitespace) {
       this.whitespace = whitespace;
     }
   }
@@ -393,7 +393,7 @@
       final String value = params.getParameter(0);
       short context;
       if ("all".equalsIgnoreCase(value)) {
-        context = AccountDiffPreference.WHOLE_FILE_CONTEXT;
+        context = DiffPreferencesInfo.WHOLE_FILE_CONTEXT;
       } else {
         try {
           context = Short.parseShort(value, 10);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
index 77f7bda..6aa1a47 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.change;
 
-import static com.google.gerrit.server.index.ChangeField.GROUP;
-
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
@@ -25,10 +23,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CommonConverters;
-import com.google.gerrit.server.change.PatchSetAncestorSorter.PatchSetData;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.GroupCollector;
-import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.change.RelatedChangesSorter.PatchSetData;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
@@ -37,7 +32,6 @@
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.revwalk.RevCommit;
 
 import java.io.IOException;
@@ -50,53 +44,36 @@
 @Singleton
 public class GetRelated implements RestReadView<RevisionResource> {
   private final Provider<ReviewDb> db;
-  private final GetRelatedByAncestors byAncestors;
   private final Provider<InternalChangeQuery> queryProvider;
-  private final PatchSetAncestorSorter sorter;
-  private final IndexCollection indexes;
-  private final boolean byAncestorsOnly;
+  private final RelatedChangesSorter sorter;
 
   @Inject
   GetRelated(Provider<ReviewDb> db,
-      @GerritServerConfig Config cfg,
-      GetRelatedByAncestors byAncestors,
       Provider<InternalChangeQuery> queryProvider,
-      PatchSetAncestorSorter sorter,
-      IndexCollection indexes) {
+      RelatedChangesSorter sorter) {
     this.db = db;
-    this.byAncestors = byAncestors;
     this.queryProvider = queryProvider;
     this.sorter = sorter;
-    this.indexes = indexes;
-    byAncestorsOnly =
-        cfg.getBoolean("change", null, "getRelatedByAncestors", false);
   }
 
   @Override
   public RelatedInfo apply(RevisionResource rsrc)
       throws RepositoryNotFoundException, IOException, OrmException {
-    List<String> thisPatchSetGroups = GroupCollector.getGroups(rsrc);
-    if (byAncestorsOnly
-        || thisPatchSetGroups == null
-        || !indexes.getSearchIndex().getSchema().hasField(GROUP)) {
-      return byAncestors.getRelated(rsrc);
-    }
     RelatedInfo relatedInfo = new RelatedInfo();
-    relatedInfo.changes = getRelated(rsrc, thisPatchSetGroups);
+    relatedInfo.changes = getRelated(rsrc);
     return relatedInfo;
   }
 
-  private List<ChangeAndCommit> getRelated(RevisionResource rsrc,
-      List<String> thisPatchSetGroups) throws OrmException, IOException {
-    if (thisPatchSetGroups.isEmpty()) {
+  private List<ChangeAndCommit> getRelated(RevisionResource rsrc)
+      throws OrmException, IOException {
+    Set<String> groups = getAllGroups(rsrc.getChange().getId());
+    if (groups.isEmpty()) {
       return Collections.emptyList();
     }
 
     List<ChangeData> cds = queryProvider.get()
         .enforceVisibility(true)
-        .byProjectGroups(
-            rsrc.getChange().getProject(),
-            getAllGroups(rsrc.getChange().getId()));
+        .byProjectGroups(rsrc.getChange().getProject(), groups);
     if (cds.isEmpty()) {
       return Collections.emptyList();
     } if (cds.size() == 1
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelatedByAncestors.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelatedByAncestors.java
deleted file mode 100644
index 09d1ef0..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelatedByAncestors.java
+++ /dev/null
@@ -1,266 +0,0 @@
-// Copyright (C) 2015 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.collect.ArrayListMultimap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.change.GetRelated.ChangeAndCommit;
-import com.google.gerrit.server.change.GetRelated.RelatedInfo;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevFlag;
-import org.eclipse.jgit.revwalk.RevSort;
-import org.eclipse.jgit.revwalk.RevWalk;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/** Implementation of {@link GetRelated} using {@link PatchSetAncestor}s. */
-class GetRelatedByAncestors {
-  private static final Logger log = LoggerFactory.getLogger(GetRelated.class);
-
-  private final GitRepositoryManager gitMgr;
-  private final Provider<ReviewDb> dbProvider;
-  private final Provider<InternalChangeQuery> queryProvider;
-
-  @Inject
-  GetRelatedByAncestors(GitRepositoryManager gitMgr,
-      Provider<ReviewDb> db,
-      Provider<InternalChangeQuery> queryProvider) {
-    this.gitMgr = gitMgr;
-    this.dbProvider = db;
-    this.queryProvider = queryProvider;
-  }
-
-  public RelatedInfo getRelated(RevisionResource rsrc)
-      throws RepositoryNotFoundException, IOException, OrmException {
-    try (Repository git = gitMgr.openRepository(rsrc.getChange().getProject());
-        RevWalk rw = new RevWalk(git)) {
-      Ref ref = git.getRefDatabase().exactRef(rsrc.getChange().getDest().get());
-      RelatedInfo info = new RelatedInfo();
-      info.changes = walk(rsrc, rw, ref);
-      return info;
-    }
-  }
-
-  private List<ChangeAndCommit> walk(RevisionResource rsrc, RevWalk rw, Ref ref)
-      throws OrmException, IOException {
-    Map<Change.Id, ChangeData> changes = allOpenChanges(rsrc);
-    Map<PatchSet.Id, PatchSet> patchSets = allPatchSets(rsrc, changes.values());
-
-    Map<String, PatchSet> commits = Maps.newHashMap();
-    for (PatchSet p : patchSets.values()) {
-      commits.put(p.getRevision().get(), p);
-    }
-
-    RevCommit rev = rw.parseCommit(ObjectId.fromString(
-        rsrc.getPatchSet().getRevision().get()));
-    rw.sort(RevSort.TOPO);
-    rw.markStart(rev);
-
-    if (ref != null && ref.getObjectId() != null) {
-      try {
-        rw.markUninteresting(rw.parseCommit(ref.getObjectId()));
-      } catch (IncorrectObjectTypeException notCommit) {
-        // Ignore and treat as new branch.
-      }
-    }
-
-    Set<Change.Id> added = Sets.newHashSet();
-    List<ChangeAndCommit> parents = Lists.newArrayList();
-    for (RevCommit c; (c = rw.next()) != null;) {
-      PatchSet p = commits.get(c.name());
-      Change g = null;
-      if (p != null) {
-        g = changes.get(p.getId().getParentKey()).change();
-        added.add(p.getId().getParentKey());
-      }
-      parents.add(new ChangeAndCommit(g, p, c));
-    }
-    List<ChangeAndCommit> list = children(rsrc, rw, changes, patchSets, added);
-    list.addAll(parents);
-
-    if (list.size() == 1) {
-      ChangeAndCommit r = list.get(0);
-      if (r.commit != null && r.commit.commit.equals(rsrc.getPatchSet().getRevision().get())) {
-        return Collections.emptyList();
-      }
-    }
-    return list;
-  }
-
-  private Map<Change.Id, ChangeData> allOpenChanges(RevisionResource rsrc)
-      throws OrmException {
-    return ChangeData.asMap(
-        queryProvider.get().byBranchOpen(rsrc.getChange().getDest()));
-  }
-
-  private Map<PatchSet.Id, PatchSet> allPatchSets(RevisionResource rsrc,
-      Collection<ChangeData> cds) throws OrmException {
-    Map<PatchSet.Id, PatchSet> r =
-        Maps.newHashMapWithExpectedSize(cds.size() * 2);
-    for (ChangeData cd : cds) {
-      for (PatchSet p : cd.patchSets()) {
-        r.put(p.getId(), p);
-      }
-    }
-
-    if (rsrc.getEdit().isPresent()) {
-      r.put(rsrc.getPatchSet().getId(), rsrc.getPatchSet());
-    }
-    return r;
-  }
-
-  private List<ChangeAndCommit> children(RevisionResource rsrc, RevWalk rw,
-      Map<Change.Id, ChangeData> changes, Map<PatchSet.Id, PatchSet> patchSets,
-      Set<Change.Id> added)
-      throws OrmException, IOException {
-    // children is a map of parent commit name to PatchSet built on it.
-    Multimap<String, PatchSet.Id> children = allChildren(changes.keySet());
-
-    RevFlag seenCommit = rw.newFlag("seenCommit");
-    LinkedList<String> q = Lists.newLinkedList();
-    seedQueue(rsrc, rw, seenCommit, patchSets, q);
-
-    ProjectControl projectCtl = rsrc.getControl().getProjectControl();
-    Set<Change.Id> seenChange = Sets.newHashSet();
-    List<ChangeAndCommit> graph = Lists.newArrayList();
-    while (!q.isEmpty()) {
-      String id = q.remove();
-
-      // For every matching change find the most recent patch set.
-      Map<Change.Id, PatchSet.Id> matches = Maps.newHashMap();
-      for (PatchSet.Id psId : children.get(id)) {
-        PatchSet.Id e = matches.get(psId.getParentKey());
-        if ((e == null || e.get() < psId.get())
-            && isVisible(projectCtl, changes, patchSets, psId))  {
-          matches.put(psId.getParentKey(), psId);
-        }
-      }
-
-      for (Map.Entry<Change.Id, PatchSet.Id> e : matches.entrySet()) {
-        ChangeData cd = changes.get(e.getKey());
-        PatchSet ps = patchSets.get(e.getValue());
-        if (cd == null || ps == null || !seenChange.add(e.getKey())) {
-          continue;
-        }
-
-        RevCommit c = rw.parseCommit(ObjectId.fromString(
-            ps.getRevision().get()));
-        if (!c.has(seenCommit)) {
-          c.add(seenCommit);
-          q.addFirst(ps.getRevision().get());
-          if (added.add(ps.getId().getParentKey())) {
-            rw.parseBody(c);
-            graph.add(new ChangeAndCommit(cd.change(), ps, c));
-          }
-        }
-      }
-    }
-    Collections.reverse(graph);
-    return graph;
-  }
-
-  private boolean isVisible(ProjectControl projectCtl,
-      Map<Change.Id, ChangeData> changes,
-      Map<PatchSet.Id, PatchSet> patchSets,
-      PatchSet.Id psId) throws OrmException {
-    ChangeData cd = changes.get(psId.getParentKey());
-    PatchSet ps = patchSets.get(psId);
-    if (cd != null && ps != null) {
-      // Related changes are in the same project, so reuse the existing
-      // ProjectControl.
-      ChangeControl ctl = projectCtl.controlFor(cd.change());
-      return ctl.isVisible(dbProvider.get())
-          && ctl.isPatchVisible(ps, dbProvider.get());
-    }
-    return false;
-  }
-
-  private void seedQueue(RevisionResource rsrc, RevWalk rw,
-      RevFlag seenCommit, Map<PatchSet.Id, PatchSet> patchSets,
-      LinkedList<String> q) throws IOException {
-    RevCommit tip = rw.parseCommit(ObjectId.fromString(
-        rsrc.getPatchSet().getRevision().get()));
-    tip.add(seenCommit);
-    q.add(tip.name());
-
-    Change.Id cId = rsrc.getChange().getId();
-    for (PatchSet p : patchSets.values()) {
-      if (cId.equals(p.getId().getParentKey())) {
-        try {
-          RevCommit c = rw.parseCommit(ObjectId.fromString(
-              p.getRevision().get()));
-          if (!c.has(seenCommit)) {
-            c.add(seenCommit);
-            q.add(c.name());
-          }
-        } catch (IOException e) {
-          log.warn(String.format(
-              "Cannot read patch set %d of %d",
-              p.getPatchSetId(), cId.get()), e);
-        }
-      }
-    }
-  }
-
-  private Multimap<String, PatchSet.Id> allChildren(Collection<Change.Id> ids)
-      throws OrmException {
-    ReviewDb db = dbProvider.get();
-    List<ResultSet<PatchSetAncestor>> t =
-        Lists.newArrayListWithCapacity(ids.size());
-    for (Change.Id id : ids) {
-      t.add(db.patchSetAncestors().byChange(id));
-    }
-
-    Multimap<String, PatchSet.Id> r = ArrayListMultimap.create();
-    for (ResultSet<PatchSetAncestor> rs : t) {
-      for (PatchSetAncestor a : rs) {
-        r.put(a.getAncestorRevision().get(), a.getPatchSet());
-      }
-    }
-    return r;
-  }
-
-
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
index 8d92beb..826ebdd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/MergeabilityCacheImpl.java
@@ -34,7 +34,7 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.strategy.SubmitStrategyFactory;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.inject.Inject;
@@ -199,7 +199,7 @@
 
     @Override
     public Boolean call()
-        throws NoSuchProjectException, MergeException, IOException {
+        throws NoSuchProjectException, IntegrationException, IOException {
       if (key.into.equals(ObjectId.zeroId())) {
         return true; // Assume yes on new branch.
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
index 68f1497..d4737bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetInserter.java
@@ -221,7 +221,6 @@
     patchSet.setRevision(new RevId(commit.name()));
     patchSet.setDraft(draft);
 
-    ChangeUtil.insertAncestors(db, patchSet.getId(), commit);
     if (groups != null) {
       patchSet.setGroups(groups);
     } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index 7a24dbc..e2473e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -17,12 +17,17 @@
 import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkNotNull;
 import static com.google.gerrit.server.PatchLineCommentsUtil.setCommentRevId;
+import static java.nio.charset.StandardCharsets.UTF_8;
 
+import com.google.auto.value.AutoValue;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
+import com.google.common.hash.HashCode;
+import com.google.common.hash.Hashing;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
@@ -75,6 +80,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
@@ -299,6 +305,32 @@
     }
   }
 
+  /**
+   * Used to compare PatchLineComments with CommentInput comments.
+   */
+  @AutoValue
+  abstract static class CommentSetEntry {
+    private static CommentSetEntry create(Patch.Key key,
+        Integer line, Side side, HashCode message, CommentRange range) {
+      return new AutoValue_PostReview_CommentSetEntry(key, line, side, message,
+          range);
+    }
+
+    public static CommentSetEntry create(PatchLineComment comment) {
+      return create(comment.getKey().getParentKey(),
+          comment.getLine(),
+          Side.fromShort(comment.getSide()),
+          Hashing.sha1().hashString(comment.getMessage(), UTF_8),
+          comment.getRange());
+    }
+
+    abstract Patch.Key key();
+    @Nullable abstract Integer line();
+    abstract Side side();
+    abstract HashCode message();
+    @Nullable abstract CommentRange range();
+  }
+
   private class Op extends BatchUpdate.Op {
     private final PatchSet.Id psId;
     private final ReviewInput in;
@@ -374,6 +406,10 @@
       List<PatchLineComment> del = Lists.newArrayList();
       List<PatchLineComment> ups = Lists.newArrayList();
 
+      Set<CommentSetEntry> existingIds = in.omitDuplicateComments
+          ? readExistingComments(ctx)
+          : Collections.<CommentSetEntry>emptySet();
+
       for (Map.Entry<String, List<CommentInput>> ent : map.entrySet()) {
         String path = ent.getKey();
         for (CommentInput c : ent.getValue()) {
@@ -381,9 +417,7 @@
           PatchLineComment e = drafts.remove(Url.decode(c.id));
           if (e == null) {
             e = new PatchLineComment(
-                new PatchLineComment.Key(
-                    new Patch.Key(psId, path),
-                    ChangeUtil.messageUUID(ctx.getDb())),
+                new PatchLineComment.Key(new Patch.Key(psId, path), null),
                 c.line != null ? c.line : 0,
                 user.getAccountId(),
                 parent, ctx.getWhen());
@@ -403,6 +437,12 @@
                 c.range.endCharacter));
             e.setLine(c.range.endLine);
           }
+          if (existingIds.contains(CommentSetEntry.create(e))) {
+            continue;
+          }
+          if (e.getKey().get() == null) {
+            e.getKey().set(ChangeUtil.messageUUID(ctx.getDb()));
+          }
           ups.add(e);
         }
       }
@@ -430,6 +470,16 @@
       return !del.isEmpty() || !ups.isEmpty();
     }
 
+    private Set<CommentSetEntry> readExistingComments(ChangeContext ctx)
+        throws OrmException {
+      Set<CommentSetEntry> r = new HashSet<>();
+      for (PatchLineComment c : plcUtil.publishedByChange(ctx.getDb(),
+            ctx.getChangeNotes())) {
+        r.add(CommentSetEntry.create(c));
+      }
+      return r;
+    }
+
     private Map<String, PatchLineComment> changeDrafts(ChangeContext ctx)
         throws OrmException {
       Map<String, PatchLineComment> drafts = Maps.newHashMap();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
index 622c99d..6820115 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PublishDraftPatchSet.java
@@ -209,6 +209,7 @@
         throw new ResourceConflictException("Patch set is not a draft");
       }
       patchSet.setDraft(false);
+      ctx.getDb().patchSets().update(Collections.singleton(patchSet));
     }
 
     private void addReviewers(ChangeContext ctx) throws OrmException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 933ac73..31bfc50 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -17,6 +17,7 @@
 import static com.google.common.base.Preconditions.checkState;
 
 import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -25,7 +26,6 @@
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
 import com.google.gerrit.server.git.BatchUpdate.RepoContext;
-import com.google.gerrit.server.git.MergeConflictException;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.project.ChangeControl;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetAncestorSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RelatedChangesSorter.java
similarity index 95%
rename from gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetAncestorSorter.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/change/RelatedChangesSorter.java
index 8b0ceb2..a0e641e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchSetAncestorSorter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RelatedChangesSorter.java
@@ -52,11 +52,11 @@
 import java.util.Set;
 
 @Singleton
-class PatchSetAncestorSorter {
+class RelatedChangesSorter {
   private final GitRepositoryManager repoManager;
 
   @Inject
-  PatchSetAncestorSorter(GitRepositoryManager repoManager) {
+  RelatedChangesSorter(GitRepositoryManager repoManager) {
     this.repoManager = repoManager;
   }
 
@@ -199,10 +199,10 @@
         }
         allPatchSets.add(psd);
       }
-      // Breadth-first search with oldest children first.
-      // TODO(dborowitz): After killing PatchSetAncestors, consider DFS to keep
-      // parallel history together.
-      pending.addAll(Lists.reverse(children.get(psd)));
+      // Depth-first search with newest children first.
+      for (PatchSetData child : children.get(psd)) {
+        pending.addFirst(child);
+      }
     }
 
     // If we saw the same change multiple times, prefer the latest patch set.
@@ -227,7 +227,7 @@
   abstract static class PatchSetData {
     @VisibleForTesting
     static PatchSetData create(ChangeData cd, PatchSet ps, RevCommit commit) {
-      return new AutoValue_PatchSetAncestorSorter_PatchSetData(cd, ps, commit);
+      return new AutoValue_RelatedChangesSorter_PatchSetData(cd, ps, commit);
     }
 
     abstract ChangeData data();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
index 2b6ee41..be9d984 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -314,19 +314,19 @@
    * The loading is performed eagerly: all values are set.
    * <p>
    * Fields marked with final or transient modifiers are skipped.
-   * <p>
-   * Boolean fields are only set when their values are true.
    *
    * @param cfg config from which the values are loaded
    * @param section section
    * @param sub subsection
    * @param s instance of class in which the values are set
    * @param defaults instance of class with default values
+   * @param i instance to merge during the load. When present, the
+   * boolean fields are not nullified when their values are false
    * @return loaded instance
    * @throws ConfigInvalidException
    */
   public static <T> T loadSection(Config cfg, String section, String sub,
-      T s, T defaults) throws ConfigInvalidException {
+      T s, T defaults, T i) throws ConfigInvalidException {
     try {
       for (Field f : s.getClass().getDeclaredFields()) {
         if (skipField(f)) {
@@ -345,7 +345,7 @@
           f.set(s, cfg.getLong(section, sub, n, (Long) d));
         } else if (isBoolean(t)) {
           boolean b = cfg.getBoolean(section, sub, n, (Boolean) d);
-          if (b) {
+          if (b || i != null) {
             f.set(s, b);
           }
         } else if (t.isEnum()) {
@@ -353,6 +353,12 @@
         } else {
           throw new ConfigInvalidException("type is unknown: " + t.getName());
         }
+        if (i != null) {
+          Object o = f.get(i);
+          if (o != null) {
+            f.set(s, o);
+          }
+        }
       }
     } catch (SecurityException | IllegalArgumentException
         | IllegalAccessException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetSummary.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetSummary.java
index c99a3af..722eb91 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GetSummary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GetSummary.java
@@ -94,9 +94,12 @@
     int tasksSleeping = 0;
     for (Task<?> task : pending) {
       switch (task.getState()) {
-        case RUNNING: tasksRunning++; break;
-        case READY: tasksReady++; break;
-        case SLEEPING: tasksSleeping++; break;
+        case RUNNING: tasksRunning++;
+          break;
+        case READY: tasksReady++;
+          break;
+        case SLEEPING: tasksSleeping++;
+          break;
         case CANCELLED:
         case DONE:
         case OTHER:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index d0c11e7..e44c810 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -227,6 +227,7 @@
   public RefUpdate.Result modifyMessage(ChangeEdit edit, String msg)
       throws AuthException, InvalidChangeOperationException, IOException,
       UnchangedCommitMessageException {
+    msg = msg.trim() + "\n";
     checkState(!Strings.isNullOrEmpty(msg), "message cannot be null");
     if (!currentUser.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index 38d3188..d52f831 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -14,8 +14,12 @@
 
 package com.google.gerrit.server.events;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Function;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
+import com.google.common.collect.Ordering;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
@@ -27,9 +31,7 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
@@ -57,56 +59,59 @@
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
 @Singleton
 public class EventFactory {
   private static final Logger log = LoggerFactory.getLogger(EventFactory.class);
+
   private final AccountCache accountCache;
   private final Provider<String> urlProvider;
   private final PatchListCache patchListCache;
-  private final SchemaFactory<ReviewDb> schema;
   private final PatchSetInfoFactory psInfoFactory;
   private final PersonIdent myIdent;
-  private final Provider<ReviewDb> db;
   private final ChangeData.Factory changeDataFactory;
   private final ApprovalsUtil approvalsUtil;
   private final ChangeKindCache changeKindCache;
+  private final Provider<InternalChangeQuery> queryProvider;
 
   @Inject
   EventFactory(AccountCache accountCache,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       PatchSetInfoFactory psif,
-      PatchListCache patchListCache, SchemaFactory<ReviewDb> schema,
+      PatchListCache patchListCache,
       @GerritPersonIdent PersonIdent myIdent,
-      Provider<ReviewDb> db,
       ChangeData.Factory changeDataFactory,
       ApprovalsUtil approvalsUtil,
-      ChangeKindCache changeKindCache) {
+      ChangeKindCache changeKindCache,
+      Provider<InternalChangeQuery> queryProvider) {
     this.accountCache = accountCache;
     this.urlProvider = urlProvider;
     this.patchListCache = patchListCache;
-    this.schema = schema;
     this.psInfoFactory = psif;
     this.myIdent = myIdent;
-    this.db = db;
     this.changeDataFactory = changeDataFactory;
     this.approvalsUtil = approvalsUtil;
     this.changeKindCache = changeKindCache;
+    this.queryProvider = queryProvider;
   }
 
   /**
@@ -116,7 +121,7 @@
    * @param change
    * @return object suitable for serialization to JSON
    */
-  public ChangeAttribute asChangeAttribute(final Change change) {
+  public ChangeAttribute asChangeAttribute(ReviewDb db, Change change) {
     ChangeAttribute a = new ChangeAttribute();
     a.project = change.getProject().get();
     a.branch = change.getDest().getShortName();
@@ -125,8 +130,7 @@
     a.number = change.getId().toString();
     a.subject = change.getSubject();
     try {
-      a.commitMessage =
-          changeDataFactory.create(db.get(), change).commitMessage();
+      a.commitMessage = changeDataFactory.create(db, change).commitMessage();
     } catch (Exception e) {
       log.error("Error while getting full commit message for"
           + " change " + a.number);
@@ -146,7 +150,8 @@
    * @param refName
    * @return object suitable for serialization to JSON
    */
-  public RefUpdateAttribute asRefUpdateAttribute(final ObjectId oldId, final ObjectId newId, final Branch.NameKey refName) {
+  public RefUpdateAttribute asRefUpdateAttribute(ObjectId oldId, ObjectId newId,
+      Branch.NameKey refName) {
     RefUpdateAttribute ru = new RefUpdateAttribute();
     ru.newRev = newId != null ? newId.getName() : ObjectId.zeroId().getName();
     ru.oldRev = oldId != null ? oldId.getName() : ObjectId.zeroId().getName();
@@ -173,10 +178,10 @@
    * @param a
    * @param notes
    */
-  public void addAllReviewers(ChangeAttribute a, ChangeNotes notes)
+  public void addAllReviewers(ReviewDb db, ChangeAttribute a, ChangeNotes notes)
       throws OrmException {
     Collection<Account.Id> reviewers =
-        approvalsUtil.getReviewers(db.get(), notes).values();
+        approvalsUtil.getReviewers(db, notes).values();
     if (!reviewers.isEmpty()) {
       a.allReviewers = Lists.newArrayListWithCapacity(reviewers.size());
       for (Account.Id id : reviewers) {
@@ -226,51 +231,17 @@
     }
   }
 
-  public void addDependencies(ChangeAttribute ca, Change change) {
+  public void addDependencies(RevWalk rw, ChangeAttribute ca, Change change,
+      PatchSet currentPs) {
+    if (change == null || currentPs == null) {
+      return;
+    }
     ca.dependsOn = new ArrayList<>();
     ca.neededBy = new ArrayList<>();
-    try (ReviewDb db = schema.open()) {
-      final PatchSet.Id psId = change.currentPatchSetId();
-      for (PatchSetAncestor a : db.patchSetAncestors().ancestorsOf(psId)) {
-        for (PatchSet p :
-            db.patchSets().byRevision(a.getAncestorRevision())) {
-          Change c = db.changes().get(p.getId().getParentKey());
-          if (c == null) {
-            log.error("Error while generating the ancestor change for"
-                + " revision " + a.getAncestorRevision() + ": Cannot find"
-                + " Change entry in database for " + p.getId().getParentKey());
-            continue;
-          }
-          ca.dependsOn.add(newDependsOn(c, p));
-        }
-      }
-
-      final PatchSet ps = db.patchSets().get(psId);
-      if (ps == null) {
-        log.error("Error while generating the list of descendants for"
-            + " PatchSet " + psId + ": Cannot find PatchSet entry in"
-            + " database.");
-      } else {
-        final RevId revId = ps.getRevision();
-        for (PatchSetAncestor a : db.patchSetAncestors().descendantsOf(revId)) {
-          final PatchSet p = db.patchSets().get(a.getPatchSet());
-          if (p == null) {
-            log.error("Error while generating the list of descendants for"
-                + " revision " + revId.get() + ": Cannot find PatchSet entry in"
-                + " database for " + a.getPatchSet());
-            continue;
-          }
-          final Change c = db.changes().get(p.getId().getParentKey());
-          if (c == null) {
-            log.error("Error while generating the list of descendants for"
-                + " revision " + revId.get() + ": Cannot find Change entry in"
-                + " database for " + p.getId().getParentKey());
-            continue;
-          }
-          ca.neededBy.add(newNeededBy(c, p));
-        }
-      }
-    } catch (OrmException e) {
+    try {
+      addDependsOn(rw, ca, change, currentPs);
+      addNeededBy(rw, ca, change, currentPs);
+    } catch (OrmException | IOException e) {
       // Squash DB exceptions and leave dependency lists partially filled.
     }
     // Remove empty lists so a confusing label won't be displayed in the output.
@@ -282,6 +253,67 @@
     }
   }
 
+  private void addDependsOn(RevWalk rw, ChangeAttribute ca, Change change,
+      PatchSet currentPs) throws OrmException, IOException {
+    RevCommit commit =
+        rw.parseCommit(ObjectId.fromString(currentPs.getRevision().get()));
+    final List<String> parentNames = new ArrayList<>(commit.getParentCount());
+    for (RevCommit p : commit.getParents()) {
+      parentNames.add(p.name());
+    }
+
+    // Find changes in this project having a patch set matching any parent of
+    // this patch set's revision.
+    for (ChangeData cd : queryProvider.get().byProjectCommits(
+        change.getProject(), parentNames)) {
+      for (PatchSet ps : cd.patchSets()) {
+        for (String p : parentNames) {
+          if (!ps.getRevision().get().equals(p)) {
+            continue;
+          }
+          ca.dependsOn.add(newDependsOn(checkNotNull(cd.change()), ps));
+        }
+      }
+    }
+    // Sort by original parent order.
+    Collections.sort(ca.dependsOn, Ordering.natural().onResultOf(
+        new Function<DependencyAttribute, Integer>() {
+          @Override
+          public Integer apply(DependencyAttribute d) {
+            for (int i = 0; i < parentNames.size(); i++) {
+              if (parentNames.get(i).equals(d.revision)) {
+                return i;
+              }
+            }
+            return parentNames.size() + 1;
+          }
+        }));
+  }
+
+  private void addNeededBy(RevWalk rw, ChangeAttribute ca, Change change,
+      PatchSet currentPs) throws OrmException, IOException {
+    if (currentPs.getGroups() == null || currentPs.getGroups().isEmpty()) {
+      return;
+    }
+    String rev = currentPs.getRevision().get();
+    // Find changes in the same related group as this patch set, having a patch
+    // set whose parent matches this patch set's revision.
+    for (ChangeData cd : queryProvider.get().byProjectGroups(
+        change.getProject(), currentPs.getGroups())) {
+      patchSets: for (PatchSet ps : cd.patchSets()) {
+        RevCommit commit =
+            rw.parseCommit(ObjectId.fromString(ps.getRevision().get()));
+        for (RevCommit p : commit.getParents()) {
+          if (!p.name().equals(rev)) {
+            continue;
+          }
+          ca.neededBy.add(newNeededBy(checkNotNull(cd.change()), ps));
+          continue patchSets;
+        }
+      }
+    }
+  }
+
   private DependencyAttribute newDependsOn(Change c, PatchSet ps) {
     DependencyAttribute d = newDependencyAttribute(c, ps);
     d.isCurrentPatchSet = ps.getId().equals(c.currentPatchSetId());
@@ -319,24 +351,21 @@
     a.commitMessage = commitMessage;
   }
 
-  public void addPatchSets(ChangeAttribute a, Collection<PatchSet> ps,
-      LabelTypes labelTypes) {
-    addPatchSets(a, ps, null, false, null, labelTypes);
-  }
-
-  public void addPatchSets(ChangeAttribute ca, Collection<PatchSet> ps,
+  public void addPatchSets(ReviewDb db, RevWalk revWalk, ChangeAttribute ca,
+      Collection<PatchSet> ps,
       Map<PatchSet.Id, Collection<PatchSetApproval>> approvals,
       LabelTypes labelTypes) {
-    addPatchSets(ca, ps, approvals, false, null, labelTypes);
+    addPatchSets(db, revWalk, ca, ps, approvals, false, null, labelTypes);
   }
 
-  public void addPatchSets(ChangeAttribute ca, Collection<PatchSet> ps,
+  public void addPatchSets(ReviewDb db, RevWalk revWalk, ChangeAttribute ca,
+      Collection<PatchSet> ps,
       Map<PatchSet.Id, Collection<PatchSetApproval>> approvals,
       boolean includeFiles, Change change, LabelTypes labelTypes) {
     if (!ps.isEmpty()) {
       ca.patchSets = new ArrayList<>(ps.size());
       for (PatchSet p : ps) {
-        PatchSetAttribute psa = asPatchSetAttribute(p);
+        PatchSetAttribute psa = asPatchSetAttribute(db, revWalk, p);
         if (approvals != null) {
           addApprovals(psa, p.getId(), approvals, labelTypes);
         }
@@ -400,7 +429,8 @@
    * @param patchSet
    * @return object suitable for serialization to JSON
    */
-  public PatchSetAttribute asPatchSetAttribute(final PatchSet patchSet) {
+  public PatchSetAttribute asPatchSetAttribute(ReviewDb db, RevWalk revWalk,
+      PatchSet patchSet) {
     PatchSetAttribute p = new PatchSetAttribute();
     p.revision = patchSet.getRevision().get();
     p.number = Integer.toString(patchSet.getPatchSetId());
@@ -408,12 +438,12 @@
     p.uploader = asAccountAttribute(patchSet.getUploader());
     p.createdOn = patchSet.getCreatedOn().getTime() / 1000L;
     p.isDraft = patchSet.isDraft();
-    final PatchSet.Id pId = patchSet.getId();
-    try (ReviewDb db = schema.open()) {
+    PatchSet.Id pId = patchSet.getId();
+    try {
       p.parents = new ArrayList<>();
-      for (PatchSetAncestor a : db.patchSetAncestors().ancestorsOf(
-          patchSet.getId())) {
-        p.parents.add(a.getAncestorRevision().get());
+      RevCommit c = revWalk.parseCommit(ObjectId.fromString(p.revision));
+      for (RevCommit parent : c.getParents()) {
+        p.parents.add(parent.name());
       }
 
       UserIdentity author = psInfoFactory.get(db, pId).getAuthor();
@@ -436,7 +466,7 @@
         }
       }
       p.kind = changeKindCache.getChangeKind(db, change, patchSet);
-    } catch (OrmException e) {
+    } catch (OrmException | IOException e) {
       log.error("Cannot load patch set data for " + patchSet.getId(), e);
     } catch (PatchSetInfoNotAvailableException e) {
       log.error(String.format("Cannot get authorEmail for %s.", pId), e);
@@ -491,7 +521,7 @@
    * @param account
    * @return object suitable for serialization to JSON
    */
-  public AccountAttribute asAccountAttribute(final Account account) {
+  public AccountAttribute asAccountAttribute(Account account) {
     if (account == null) {
       return null;
     }
@@ -560,9 +590,9 @@
   }
 
   /** Get a link to the change; null if the server doesn't know its own address. */
-  private String getChangeUrl(final Change change) {
+  private String getChangeUrl(Change change) {
     if (change != null && urlProvider.get() != null) {
-      final StringBuilder r = new StringBuilder();
+      StringBuilder r = new StringBuilder();
       r.append(urlProvider.get());
       r.append(change.getChangeId());
       return r.toString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/IntegrationException.java
similarity index 73%
rename from gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/git/IntegrationException.java
index d3ebb95..58d4e6e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/IntegrationException.java
@@ -14,19 +14,19 @@
 
 package com.google.gerrit.server.git;
 
-/** Indicates the current branch's queue cannot be processed at this time. */
-public class MergeException extends Exception {
+/** Indicates an integration operation (see {@link MergeOp}) failed. */
+public class IntegrationException extends Exception {
   private static final long serialVersionUID = 1L;
 
-  public MergeException(String msg) {
+  public IntegrationException(String msg) {
     super(msg);
   }
 
-  public MergeException(Throwable why) {
+  public IntegrationException(Throwable why) {
     super(why);
   }
 
-  public MergeException(String msg, Throwable why) {
+  public IntegrationException(String msg, Throwable why) {
     super(msg, why);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 624e30c..eebae70 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -201,7 +201,7 @@
   public Repository createRepository(Project.NameKey name)
       throws RepositoryNotFoundException, RepositoryCaseMismatchException {
     Repository repo = createRepository(basePath, name);
-    if (noteDbPath != null) {
+    if (noteDbPath != null && !noteDbPath.equals(basePath)) {
       createRepository(noteDbPath, name);
     }
     return repo;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 776cee9..74474f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -226,10 +226,12 @@
     mergeTips = new HashMap<>();
   }
 
-  private void setDestProject(Branch.NameKey destBranch) throws MergeException {
+  private void setDestProject(Branch.NameKey destBranch)
+      throws IntegrationException {
     destProject = projectCache.get(destBranch.getParentKey());
     if (destProject == null) {
-      throw new MergeException("No such project: " + destBranch.getParentKey());
+      throw new IntegrationException(
+          "No such project: " + destBranch.getParentKey());
     }
   }
 
@@ -376,7 +378,7 @@
       }
       try {
         integrateIntoHistory(cs, caller);
-      } catch (MergeException e) {
+      } catch (IntegrationException e) {
         logError("Merge Conflict", e);
         throw new ResourceConflictException("Merge Conflict", e);
       }
@@ -387,7 +389,8 @@
   }
 
   private void integrateIntoHistory(ChangeSet cs, IdentifiedUser caller)
-      throws MergeException, NoSuchChangeException, ResourceConflictException {
+      throws IntegrationException, NoSuchChangeException,
+      ResourceConflictException {
     logDebug("Beginning merge attempt on {}", cs);
     Map<Branch.NameKey, ListMultimap<SubmitType, ChangeData>> toSubmit =
         new HashMap<>();
@@ -450,9 +453,9 @@
           + "abandoning open changes");
       abandonAllOpenChanges(noProject.project());
     } catch (OrmException e) {
-      throw new MergeException("Cannot query the database", e);
+      throw new IntegrationException("Cannot query the database", e);
     } catch (IOException e) {
-      throw new MergeException("Cannot query the database", e);
+      throw new IntegrationException("Cannot query the database", e);
     } finally {
       closeRepository();
     }
@@ -460,7 +463,7 @@
 
   private MergeTip preMerge(SubmitStrategy strategy,
       List<ChangeData> submitted, CodeReviewCommit branchTip)
-      throws MergeException, OrmException {
+      throws IntegrationException, OrmException {
     logDebug("Running submit strategy {} for {} commits {}",
         strategy.getClass().getSimpleName(), submitted.size(), submitted);
     List<CodeReviewCommit> toMerge = new ArrayList<>(submitted.size());
@@ -479,20 +482,20 @@
 
   private SubmitStrategy createStrategy(Branch.NameKey destBranch,
       SubmitType submitType, CodeReviewCommit branchTip, IdentifiedUser caller)
-      throws MergeException, NoSuchProjectException {
+      throws IntegrationException, NoSuchProjectException {
     return submitStrategyFactory.create(submitType, db, repo, rw, inserter,
         canMergeFlag, getAlreadyAccepted(branchTip), destBranch, caller);
   }
 
   private void openRepository(Project.NameKey name)
-      throws MergeException, NoSuchProjectException {
+      throws IntegrationException, NoSuchProjectException {
     try {
       repo = repoManager.openRepository(name);
     } catch (RepositoryNotFoundException notFound) {
       throw new NoSuchProjectException(name, notFound);
     } catch (IOException err) {
       String m = "Error opening repository \"" + name.get() + '"';
-      throw new MergeException(m, err);
+      throw new IntegrationException(m, err);
     }
 
     rw = CodeReviewCommit.newRevWalk(repo);
@@ -517,7 +520,7 @@
   }
 
   private RefUpdate getPendingRefUpdate(Branch.NameKey destBranch)
-      throws MergeException {
+      throws IntegrationException {
 
     if (pendingRefUpdates.containsKey(destBranch)) {
       logDebug("Access cached open branch {}: {}", destBranch.get(),
@@ -534,8 +537,8 @@
         branchTip = null;
         branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
       } else {
-        throw new MergeException("The destination branch " + destBranch.get()
-            + " does not exist anymore.");
+        throw new IntegrationException("The destination branch "
+            + destBranch.get() + " does not exist anymore.");
       }
 
       logDebug("Opened branch {}: {}", destBranch.get(), branchTip);
@@ -543,12 +546,12 @@
       openBranches.put(destBranch, branchTip);
       return branchUpdate;
     } catch (IOException e) {
-      throw new MergeException("Cannot open branch", e);
+      throw new IntegrationException("Cannot open branch", e);
     }
   }
 
   private CodeReviewCommit getBranchTip(Branch.NameKey destBranch)
-      throws MergeException {
+      throws IntegrationException {
     if (openBranches.containsKey(destBranch)) {
       return openBranches.get(destBranch);
     } else {
@@ -558,7 +561,7 @@
   }
 
   private Set<RevCommit> getAlreadyAccepted(CodeReviewCommit branchTip)
-      throws MergeException {
+      throws IntegrationException {
     Set<RevCommit> alreadyAccepted = new HashSet<>();
 
     if (branchTip != null) {
@@ -574,7 +577,7 @@
         }
       }
     } catch (IOException e) {
-      throw new MergeException(
+      throw new IntegrationException(
           "Failed to determine already accepted commits.", e);
     }
 
@@ -583,7 +586,7 @@
   }
 
   private ListMultimap<SubmitType, ChangeData> validateChangeList(
-      Collection<ChangeData> submitted) throws MergeException {
+      Collection<ChangeData> submitted) throws IntegrationException {
     logDebug("Validating {} changes", submitted.size());
     ListMultimap<SubmitType, ChangeData> toSubmit = ArrayListMultimap.create();
 
@@ -591,7 +594,7 @@
     try {
       allRefs = repo.getRefDatabase().getRefs(ALL);
     } catch (IOException e) {
-      throw new MergeException(e.getMessage(), e);
+      throw new IntegrationException(e.getMessage(), e);
     }
 
     Set<ObjectId> tips = new HashSet<>();
@@ -607,7 +610,7 @@
         // Reload change in case index was stale.
         chg = cd.reloadChange();
       } catch (OrmException e) {
-        throw new MergeException("Failed to validate changes", e);
+        throw new IntegrationException("Failed to validate changes", e);
       }
       Change.Id changeId = cd.getId();
       if (chg.getStatus() != Change.Status.NEW) {
@@ -625,7 +628,7 @@
       try {
         ps = cd.currentPatchSet();
       } catch (OrmException e) {
-        throw new MergeException("Cannot query the database", e);
+        throw new IntegrationException("Cannot query the database", e);
       }
       if (ps == null || ps.getRevision() == null
           || ps.getRevision().get() == null) {
@@ -718,7 +721,7 @@
   }
 
   private RefUpdate updateBranch(Branch.NameKey destBranch)
-      throws MergeException {
+      throws IntegrationException {
     RefUpdate branchUpdate = getPendingRefUpdate(destBranch);
     CodeReviewCommit branchTip = getBranchTip(destBranch);
 
@@ -746,7 +749,7 @@
             new ProjectConfig(destProject.getProject().getNameKey());
         cfg.load(repo, currentTip);
       } catch (Exception e) {
-        throw new MergeException("Submit would store invalid"
+        throw new IntegrationException("Submit would store invalid"
             + " project configuration " + currentTip.name() + " for "
             + destProject.getProject().getName(), e);
       }
@@ -782,13 +785,13 @@
           return branchUpdate;
 
         case LOCK_FAILURE:
-          throw new MergeException("Failed to lock " + branchUpdate.getName());
+          throw new IntegrationException("Failed to lock " + branchUpdate.getName());
         default:
           throw new IOException(branchUpdate.getResult().name()
               + '\n' + branchUpdate);
       }
     } catch (IOException e) {
-      throw new MergeException("Cannot update " + branchUpdate.getName(), e);
+      throw new IntegrationException("Cannot update " + branchUpdate.getName(), e);
     }
   }
 
@@ -820,7 +823,7 @@
 
   private void updateChangeStatus(List<ChangeData> submitted,
       Branch.NameKey destBranch, boolean dryRun, IdentifiedUser caller)
-      throws NoSuchChangeException, MergeException, ResourceConflictException,
+      throws NoSuchChangeException, IntegrationException, ResourceConflictException,
       OrmException {
     if (!dryRun) {
       logDebug("Updating change status for {} changes", submitted.size());
@@ -896,8 +899,8 @@
 
           case MISSING_DEPENDENCY:
             logDebug("Change {} is missing dependency", c.getId());
-            throw new MergeException("Cannot merge " + commit.name() + "\n"
-                + s.getMessage());
+            throw new IntegrationException(
+                "Cannot merge " + commit.name() + "\n" + s.getMessage());
 
           case REVISION_GONE:
             logDebug("Commit not found for change {}", c.getId());
@@ -910,12 +913,12 @@
                 c.currentPatchSetId());
             msg.setMessage("Failed to read commit for this patch set");
             setNew(commit.notes(), msg);
-            throw new MergeException(msg.getMessage());
+            throw new IntegrationException(msg.getMessage());
 
           default:
             msg = message(c, "Unspecified merge failure: " + s.name());
             setNew(commit.notes(), msg);
-            throw new MergeException(msg.getMessage());
+            throw new IntegrationException(msg.getMessage());
         }
       } catch (OrmException | IOException err) {
         logWarn("Error updating change status for " + c.getId(), err);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
index 5fdefbe..436147c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.common.FooterConstants;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -138,7 +139,7 @@
 
   public CodeReviewCommit getFirstFastForward(
       final CodeReviewCommit mergeTip, final RevWalk rw,
-      final List<CodeReviewCommit> toMerge) throws MergeException {
+      final List<CodeReviewCommit> toMerge) throws IntegrationException {
     for (final Iterator<CodeReviewCommit> i = toMerge.iterator(); i.hasNext();) {
       try {
         final CodeReviewCommit n = i.next();
@@ -147,19 +148,20 @@
           return n;
         }
       } catch (IOException e) {
-        throw new MergeException("Cannot fast-forward test during merge", e);
+        throw new IntegrationException(
+            "Cannot fast-forward test during merge", e);
       }
     }
     return mergeTip;
   }
 
   public List<CodeReviewCommit> reduceToMinimalMerge(MergeSorter mergeSorter,
-      Collection<CodeReviewCommit> toSort) throws MergeException {
+      Collection<CodeReviewCommit> toSort) throws IntegrationException {
     List<CodeReviewCommit> result = new ArrayList<>();
     try {
       result.addAll(mergeSorter.sort(toSort));
     } catch (IOException e) {
-      throw new MergeException("Branch head sorting failed", e);
+      throw new IntegrationException("Branch head sorting failed", e);
     }
     Collections.sort(result, CodeReviewCommit.ORDER);
     return result;
@@ -345,7 +347,7 @@
   public boolean canMerge(final MergeSorter mergeSorter,
       final Repository repo, final CodeReviewCommit mergeTip,
       final CodeReviewCommit toMerge)
-      throws MergeException {
+      throws IntegrationException {
     if (hasMissingDependencies(mergeSorter, toMerge)) {
       return false;
     }
@@ -359,13 +361,13 @@
     } catch (NoMergeBaseException e) {
       return false;
     } catch (IOException e) {
-      throw new MergeException("Cannot merge " + toMerge.name(), e);
+      throw new IntegrationException("Cannot merge " + toMerge.name(), e);
     }
   }
 
   public boolean canFastForward(MergeSorter mergeSorter,
       CodeReviewCommit mergeTip, CodeReviewRevWalk rw, CodeReviewCommit toMerge)
-      throws MergeException {
+      throws IntegrationException {
     if (hasMissingDependencies(mergeSorter, toMerge)) {
       return false;
     }
@@ -373,13 +375,13 @@
     try {
       return mergeTip == null || rw.isMergedInto(mergeTip, toMerge);
     } catch (IOException e) {
-      throw new MergeException("Cannot fast-forward test during merge", e);
+      throw new IntegrationException("Cannot fast-forward test during merge", e);
     }
   }
 
   public boolean canCherryPick(MergeSorter mergeSorter, Repository repo,
       CodeReviewCommit mergeTip, CodeReviewRevWalk rw, CodeReviewCommit toMerge)
-      throws MergeException {
+      throws IntegrationException {
     if (mergeTip == null) {
       // The branch is unborn. Fast-forward is possible.
       //
@@ -403,7 +405,7 @@
         m.setBase(toMerge.getParent(0));
         return m.merge(mergeTip, toMerge);
       } catch (IOException e) {
-        throw new MergeException("Cannot merge " + toMerge.name(), e);
+        throw new IntegrationException("Cannot merge " + toMerge.name(), e);
       }
     }
 
@@ -418,11 +420,11 @@
   }
 
   public boolean hasMissingDependencies(final MergeSorter mergeSorter,
-      final CodeReviewCommit toMerge) throws MergeException {
+      final CodeReviewCommit toMerge) throws IntegrationException {
     try {
       return !mergeSorter.sort(Collections.singleton(toMerge)).contains(toMerge);
     } catch (IOException e) {
-      throw new MergeException("Branch head sorting failed", e);
+      throw new IntegrationException("Branch head sorting failed", e);
     }
   }
 
@@ -449,7 +451,8 @@
   public CodeReviewCommit mergeOneCommit(PersonIdent author,
       PersonIdent committer, Repository repo, CodeReviewRevWalk rw,
       ObjectInserter inserter, RevFlag canMergeFlag, Branch.NameKey destBranch,
-      CodeReviewCommit mergeTip, CodeReviewCommit n) throws MergeException {
+      CodeReviewCommit mergeTip, CodeReviewCommit n)
+      throws IntegrationException {
     final ThreeWayMerger m = newThreeWayMerger(repo, inserter);
     try {
       if (m.merge(new AnyObjectId[] {mergeTip, n})) {
@@ -463,10 +466,10 @@
         failed(rw, canMergeFlag, mergeTip, n,
             getCommitMergeStatus(e.getReason()));
       } catch (IOException e2) {
-        throw new MergeException("Cannot merge " + n.name(), e);
+        throw new IntegrationException("Cannot merge " + n.name(), e);
       }
     } catch (IOException e) {
-      throw new MergeException("Cannot merge " + n.name(), e);
+      throw new IntegrationException("Cannot merge " + n.name(), e);
     }
     return mergeTip;
   }
@@ -639,7 +642,7 @@
 
   public void markCleanMerges(final RevWalk rw,
       final RevFlag canMergeFlag, final CodeReviewCommit mergeTip,
-      final Set<RevCommit> alreadyAccepted) throws MergeException {
+      final Set<RevCommit> alreadyAccepted) throws IntegrationException {
     if (mergeTip == null) {
       // If mergeTip is null here, branchTip was null, indicating a new branch
       // at the start of the merge process. We also elected to merge nothing,
@@ -664,7 +667,7 @@
         }
       }
     } catch (IOException e) {
-      throw new MergeException("Cannot mark clean merges", e);
+      throw new IntegrationException("Cannot mark clean merges", e);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index d12d30a..addbc1b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -118,6 +118,7 @@
       "requireContributorAgreement";
   private static final String KEY_CHECK_RECEIVED_OBJECTS = "checkReceivedObjects";
   private static final String KEY_ENABLE_SIGNED_PUSH = "enableSignedPush";
+  private static final String KEY_REQUIRE_SIGNED_PUSH = "requireSignedPush";
 
   private static final String SUBMIT = "submit";
   private static final String KEY_ACTION = "action";
@@ -420,6 +421,8 @@
     p.setCreateNewChangeForAllNotInTarget(getEnum(rc, RECEIVE, null, KEY_USE_ALL_NOT_IN_TARGET, InheritableBoolean.INHERIT));
     p.setEnableSignedPush(getEnum(rc, RECEIVE, null,
           KEY_ENABLE_SIGNED_PUSH, InheritableBoolean.INHERIT));
+    p.setRequireSignedPush(getEnum(rc, RECEIVE, null,
+          KEY_REQUIRE_SIGNED_PUSH, InheritableBoolean.INHERIT));
     p.setMaxObjectSizeLimit(rc.getString(RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT));
 
     p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, defaultSubmitAction));
@@ -775,6 +778,17 @@
       Config pluginConfig = new Config();
       pluginConfigs.put(plugin, pluginConfig);
       for (String name : rc.getNames(PLUGIN, plugin)) {
+        String value = rc.getString(PLUGIN, plugin, name);
+        if (value.startsWith("Group[")) {
+          GroupReference refFromString = GroupReference.fromString(value);
+          GroupReference ref = groupList.byUUID(refFromString.getUUID());
+          if (ref == null) {
+            ref = refFromString;
+            error(new ValidationError(PROJECT_CONFIG,
+                "group \"" + ref.getName() + "\" not in " + GroupList.FILE_NAME));
+          }
+          rc.setString(PLUGIN, plugin, name, ref.toString());
+        }
         pluginConfig.setStringList(PLUGIN, plugin, name,
             Arrays.asList(rc.getStringList(PLUGIN, plugin, name)));
       }
@@ -828,6 +842,8 @@
     set(rc, RECEIVE, null, KEY_MAX_OBJECT_SIZE_LIMIT, validMaxObjectSizeLimit(p.getMaxObjectSizeLimit()));
     set(rc, RECEIVE, null, KEY_ENABLE_SIGNED_PUSH,
         p.getEnableSignedPush(), InheritableBoolean.INHERIT);
+    set(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_PUSH,
+        p.getRequireSignedPush(), InheritableBoolean.INHERIT);
 
     set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), defaultSubmitAction);
     set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.getUseContentMerge(), InheritableBoolean.INHERIT);
@@ -842,9 +858,9 @@
     saveContributorAgreements(rc, keepGroups);
     saveAccessSections(rc, keepGroups);
     saveNotifySections(rc, keepGroups);
+    savePluginSections(rc, keepGroups);
     groupList.retainUUIDs(keepGroups);
     saveLabelSections(rc);
-    savePluginSections(rc);
 
     saveConfig(PROJECT_CONFIG, rc);
     saveGroupList();
@@ -1096,7 +1112,7 @@
     }
   }
 
-  private void savePluginSections(Config rc) {
+  private void savePluginSections(Config rc, Set<AccountGroup.UUID> keepGroups) {
     List<String> existing = Lists.newArrayList(rc.getSubsections(PLUGIN));
     for (String name : existing) {
       rc.unsetSection(PLUGIN, name);
@@ -1106,6 +1122,14 @@
       String plugin = e.getKey();
       Config pluginConfig = e.getValue();
       for (String name : pluginConfig.getNames(PLUGIN, plugin)) {
+        String value = pluginConfig.getString(PLUGIN, plugin, name);
+        if (value.startsWith("Group[")) {
+          GroupReference ref = resolve(GroupReference.fromString(value));
+          if (ref.getUUID() != null) {
+            keepGroups.add(ref.getUUID());
+            pluginConfig.setString(PLUGIN, plugin, name, ref.toString());
+          }
+        }
         rc.setStringList(PLUGIN, plugin, name,
             Arrays.asList(pluginConfig.getStringList(PLUGIN, plugin, name)));
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index c87ed00..5da4ec7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -2252,7 +2252,6 @@
           return null;
         }
 
-        ChangeUtil.insertAncestors(db, newPatchSet.getId(), newCommit);
         if (newPatchSet.getGroups() == null) {
           newPatchSet.setGroups(GroupCollector.getCurrentGroups(db, change));
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
index 29b5373..a1c5b8a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/UserConfigSections.java
@@ -22,6 +22,9 @@
   /** The edit user preferences. */
   public static final String EDIT = "edit";
 
+  /** The diff user preferences. */
+  public static final String DIFF = "diff";
+
   private UserConfigSections() {
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
index a2e9c03..61b1b00 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/CherryPick.java
@@ -16,14 +16,13 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
@@ -31,8 +30,7 @@
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
 import com.google.gerrit.server.git.GroupCollector;
-import com.google.gerrit.server.git.MergeConflictException;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeIdenticalTreeException;
 import com.google.gerrit.server.git.MergeTip;
 import com.google.gerrit.server.git.UpdateException;
@@ -42,11 +40,9 @@
 
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.transport.ReceiveCommand;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -66,7 +62,7 @@
 
   @Override
   protected MergeTip _run(CodeReviewCommit branchTip,
-      Collection<CodeReviewCommit> toMerge) throws MergeException {
+      Collection<CodeReviewCommit> toMerge) throws IntegrationException {
     MergeTip mergeTip = new MergeTip(branchTip, toMerge);
     List<CodeReviewCommit> sorted = CodeReviewCommit.ORDER.sortedCopy(toMerge);
     boolean first = true;
@@ -87,7 +83,8 @@
       }
       u.execute();
     } catch (UpdateException | RestApiException e) {
-      throw new MergeException("Cannot cherry-pick onto " + args.destBranch);
+      throw new IntegrationException(
+          "Cannot cherry-pick onto " + args.destBranch);
     }
     // TODO(dborowitz): When BatchUpdate is hoisted out of CherryPick,
     // SubmitStrategy should probably no longer return MergeTip, instead just
@@ -187,7 +184,6 @@
       Change c = toMerge.change();
       ps.setGroups(GroupCollector.getCurrentGroups(args.db, c));
       args.db.patchSets().insert(Collections.singleton(ps));
-      insertAncestors(args.db, ps.getId(), newCommit);
       c.setCurrentPatchSet(patchSetInfo);
       args.db.changes().update(Collections.singletonList(c));
 
@@ -219,7 +215,8 @@
     }
 
     @Override
-    public void updateRepo(RepoContext ctx) throws MergeException, IOException {
+    public void updateRepo(RepoContext ctx)
+        throws IntegrationException, IOException {
       if (args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge)) {
         // One or more dependencies were not met. The status was already marked
         // on the commit so we have nothing further to perform at this time.
@@ -247,20 +244,6 @@
     }
   }
 
-  private static void insertAncestors(ReviewDb db, PatchSet.Id id,
-      RevCommit src) throws OrmException {
-    int cnt = src.getParentCount();
-    List<PatchSetAncestor> toInsert = new ArrayList<>(cnt);
-    for (int p = 0; p < cnt; p++) {
-      PatchSetAncestor a;
-
-      a = new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1));
-      a.setAncestorRevision(new RevId(src.getParent(p).getId().name()));
-      toInsert.add(a);
-    }
-    db.patchSetAncestors().insert(toInsert);
-  }
-
   @Override
   public Map<Change.Id, CodeReviewCommit> getNewCommits() {
     return newCommits;
@@ -268,7 +251,7 @@
 
   @Override
   public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
-      throws MergeException {
+      throws IntegrationException {
     return args.mergeUtil.canCherryPick(args.mergeSorter, args.repo,
         mergeTip, args.rw, toMerge);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
index f7d8ab1..1709659 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/FastForwardOnly.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeTip;
 
 import java.util.Collection;
@@ -29,7 +29,7 @@
 
   @Override
   protected MergeTip _run(final CodeReviewCommit branchTip,
-      final Collection<CodeReviewCommit> toMerge) throws MergeException {
+      final Collection<CodeReviewCommit> toMerge) throws IntegrationException {
     MergeTip mergeTip = new MergeTip(branchTip, toMerge);
     List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(
         args.mergeSorter, toMerge);
@@ -56,7 +56,7 @@
 
   @Override
   public boolean dryRun(CodeReviewCommit mergeTip,
-      CodeReviewCommit toMerge) throws MergeException {
+      CodeReviewCommit toMerge) throws IntegrationException {
     return args.mergeUtil.canFastForward(args.mergeSorter, mergeTip, args.rw,
         toMerge);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
index d3a72e3..9f5f521 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeAlways.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.git.strategy;
 
 import com.google.gerrit.server.git.CodeReviewCommit;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeTip;
 
 import org.eclipse.jgit.lib.PersonIdent;
@@ -30,7 +30,7 @@
 
   @Override
   protected MergeTip _run(CodeReviewCommit branchTip,
-      Collection<CodeReviewCommit> toMerge) throws MergeException {
+      Collection<CodeReviewCommit> toMerge) throws IntegrationException {
   List<CodeReviewCommit> sorted = args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
     MergeTip mergeTip;
     if (branchTip == null) {
@@ -62,7 +62,7 @@
 
   @Override
   public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
-      throws MergeException {
+      throws IntegrationException {
     return args.mergeUtil.canMerge(args.mergeSorter, args.repo, mergeTip,
         toMerge);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
index 688fa3e..4ebe461 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/MergeIfNecessary.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.git.strategy;
 
 import com.google.gerrit.server.git.CodeReviewCommit;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeTip;
 
 import org.eclipse.jgit.lib.PersonIdent;
@@ -30,7 +30,7 @@
 
   @Override
   protected MergeTip _run(CodeReviewCommit branchTip,
-      Collection<CodeReviewCommit> toMerge) throws MergeException {
+      Collection<CodeReviewCommit> toMerge) throws IntegrationException {
     List<CodeReviewCommit> sorted =
         args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
     MergeTip mergeTip;
@@ -67,7 +67,7 @@
 
   @Override
   public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
-      throws MergeException {
+      throws IntegrationException {
     return args.mergeUtil.canFastForward(
           args.mergeSorter, mergeTip, args.rw, toMerge)
         || args.mergeUtil.canMerge(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
index 5ef33a6..cd6b5bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/RebaseIfNecessary.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.MergeConflictException;
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -24,8 +25,7 @@
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CommitMergeStatus;
-import com.google.gerrit.server.git.MergeConflictException;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeTip;
 import com.google.gerrit.server.git.RebaseSorter;
 import com.google.gerrit.server.git.UpdateException;
@@ -60,7 +60,7 @@
 
   @Override
   protected MergeTip _run(final CodeReviewCommit branchTip,
-      final Collection<CodeReviewCommit> toMerge) throws MergeException {
+      final Collection<CodeReviewCommit> toMerge) throws IntegrationException {
     MergeTip mergeTip = new MergeTip(branchTip, toMerge);
     List<CodeReviewCommit> sorted = sort(toMerge);
     while (!sorted.isEmpty()) {
@@ -111,16 +111,13 @@
             newCommits.put(newPatchSet.getId().getParentKey(),
                 mergeTip.getCurrentTip());
             setRefLogIdent();
-          } catch (UpdateException e) {
-            if (e.getCause() instanceof MergeConflictException) {
-              n.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
-            }
-            throw new MergeException("Cannot rebase " + n.name(), e);
+          } catch (MergeConflictException e) {
+            n.setStatusCode(CommitMergeStatus.REBASE_MERGE_CONFLICT);
+            throw new IntegrationException(
+                "Cannot rebase " + n.name() + ": " + e.getMessage(), e);
           } catch (NoSuchChangeException | OrmException | IOException
-              | RestApiException e) {
-            // TODO(dborowitz): Allow Submit to unwrap ResourceConflictException
-            // so it can turn into a 409.
-            throw new MergeException("Cannot rebase " + n.name(), e);
+              | RestApiException | UpdateException e) {
+            throw new IntegrationException("Cannot rebase " + n.name(), e);
           }
         }
 
@@ -145,7 +142,7 @@
               mergeTip.getCurrentTip(), args.alreadyAccepted);
           setRefLogIdent();
         } catch (IOException e) {
-          throw new MergeException("Cannot merge " + n.name(), e);
+          throw new IntegrationException("Cannot merge " + n.name(), e);
         }
       }
 
@@ -156,14 +153,14 @@
   }
 
   private List<CodeReviewCommit> sort(Collection<CodeReviewCommit> toSort)
-      throws MergeException {
+      throws IntegrationException {
     try {
       List<CodeReviewCommit> result = new RebaseSorter(
           args.rw, args.alreadyAccepted, args.canMergeFlag).sort(toSort);
       Collections.sort(result, CodeReviewCommit.ORDER);
       return result;
     } catch (IOException e) {
-      throw new MergeException("Commit sorting failed", e);
+      throw new IntegrationException("Commit sorting failed", e);
     }
   }
 
@@ -190,7 +187,7 @@
 
   @Override
   public boolean dryRun(CodeReviewCommit mergeTip, CodeReviewCommit toMerge)
-      throws MergeException {
+      throws IntegrationException {
     return !args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge)
         && args.mergeUtil.canCherryPick(args.mergeSorter, args.repo, mergeTip,
             args.rw, toMerge);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
index e3247c7..5215f55 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
@@ -25,7 +25,7 @@
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeSorter;
 import com.google.gerrit.server.git.MergeTip;
 import com.google.gerrit.server.git.MergeUtil;
@@ -124,10 +124,10 @@
    *        this submit strategy. Implementations are responsible for ordering
    *        of commits, and should not modify the input in place.
    * @return the new merge tip.
-   * @throws MergeException
+   * @throws IntegrationException
    */
   public final MergeTip run(final CodeReviewCommit currentTip,
-      final Collection<CodeReviewCommit> toMerge) throws MergeException {
+      final Collection<CodeReviewCommit> toMerge) throws IntegrationException {
     refLogIdent = null;
     checkState(args.caller != null);
     return _run(currentTip, toMerge);
@@ -135,7 +135,7 @@
 
   /** @see #run(CodeReviewCommit, Collection) */
   protected abstract MergeTip _run(CodeReviewCommit currentTip,
-      Collection<CodeReviewCommit> toMerge) throws MergeException;
+      Collection<CodeReviewCommit> toMerge) throws IntegrationException;
 
   /**
    * Checks whether the given commit can be merged.
@@ -147,10 +147,10 @@
    * @param toMerge the commit that should be checked.
    * @return {@code true} if the given commit can be merged, otherwise
    *         {@code false}
-   * @throws MergeException
+   * @throws IntegrationException
    */
   public abstract boolean dryRun(CodeReviewCommit mergeTip,
-      CodeReviewCommit toMerge) throws MergeException;
+      CodeReviewCommit toMerge) throws IntegrationException;
 
   /**
    * Returns the identity that should be used for reflog entries when updating
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
index 05aee25..0c8c2f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategyFactory.java
@@ -23,7 +23,7 @@
 import com.google.gerrit.server.change.RebaseChangeOp;
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.index.ChangeIndexer;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -90,7 +90,7 @@
       Repository repo, CodeReviewRevWalk rw, ObjectInserter inserter,
       RevFlag canMergeFlag, Set<RevCommit> alreadyAccepted,
       Branch.NameKey destBranch, IdentifiedUser caller)
-      throws MergeException, NoSuchProjectException {
+      throws IntegrationException, NoSuchProjectException {
     ProjectState project = getProject(destBranch);
     SubmitStrategy.Arguments args = new SubmitStrategy.Arguments(
         identifiedUserFactory, myIdent, db, batchUpdateFactory,
@@ -111,7 +111,7 @@
       default:
         final String errorMsg = "No submit strategy for: " + submitType;
         log.error(errorMsg);
-        throw new MergeException(errorMsg);
+        throw new IntegrationException(errorMsg);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
index 3b04f05..02e737a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/ChangeIndex.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.QueryOptions;
 
 import java.io.IOException;
 
@@ -81,16 +82,16 @@
    * @param p the predicate to match. Must be a tree containing only AND, OR,
    *     or NOT predicates as internal nodes, and {@link IndexPredicate}s as
    *     leaves.
-   * @param start offset in results list at which to start returning results.
-   * @param limit maximum number of results to return.
+   * @param opts query options not implied by the predicate, such as start and
+   *     limit.
    * @return a source of documents matching the predicate. Documents must be
    *     returned in descending updated timestamp order.
    *
    * @throws QueryParseException if the predicate could not be converted to an
    *     indexed data source.
    */
-  public ChangeDataSource getSource(Predicate<ChangeData> p, int start,
-      int limit) throws QueryParseException;
+  public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts)
+      throws QueryParseException;
 
   /**
    * Mark whether this index is up-to-date and ready to serve reads.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndex.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndex.java
index 09226bd..d204905 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndex.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/DummyIndex.java
@@ -16,9 +16,9 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.QueryOptions;
 
 import java.io.IOException;
 
@@ -45,8 +45,7 @@
   }
 
   @Override
-  public ChangeDataSource getSource(Predicate<ChangeData> p, int start,
-      int limit) throws QueryParseException {
+  public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts) {
     throw new UnsupportedOperationException();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
index afb7c22..2799144 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexModule.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.query.change.ChangeQueryRewriter;
 import com.google.inject.Injector;
 import com.google.inject.Key;
 import com.google.inject.Provides;
@@ -67,7 +66,7 @@
 
   @Override
   protected void configure() {
-    bind(ChangeQueryRewriter.class).to(IndexRewriteImpl.class);
+    bind(IndexRewriter.class);
     bind(IndexCollection.class);
     listener().to(IndexCollection.class);
     factory(ChangeIndexer.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriter.java
similarity index 88%
rename from gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriter.java
index 8dd7aa3..d56348b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriter.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.index;
 
-import static com.google.common.base.Preconditions.checkArgument;
-
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Change;
@@ -27,11 +25,12 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.AndSource;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryRewriter;
 import com.google.gerrit.server.query.change.ChangeStatusPredicate;
 import com.google.gerrit.server.query.change.LimitPredicate;
 import com.google.gerrit.server.query.change.OrSource;
+import com.google.gerrit.server.query.change.QueryOptions;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
 import org.eclipse.jgit.util.MutableInteger;
 
@@ -41,7 +40,8 @@
 import java.util.Set;
 
 /** Rewriter that pushes boolean logic into the secondary index. */
-public class IndexRewriteImpl implements ChangeQueryRewriter {
+@Singleton
+public class IndexRewriter {
   /** Set of all open change statuses. */
   public static final Set<Change.Status> OPEN_STATUSES;
 
@@ -122,25 +122,20 @@
   private final IndexConfig config;
 
   @Inject
-  IndexRewriteImpl(IndexCollection indexes,
+  IndexRewriter(IndexCollection indexes,
       IndexConfig config) {
     this.indexes = indexes;
     this.config = config;
   }
 
-  @Override
-  public Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start,
-      int limit) throws QueryParseException {
-    checkArgument(limit > 0, "limit must be positive: %s", limit);
+  public Predicate<ChangeData> rewrite(Predicate<ChangeData> in,
+      QueryOptions opts) throws QueryParseException {
     ChangeIndex index = indexes.getSearchIndex();
-    // Increase the limit rather than skipping, since we don't know how many
-    // skipped results would have been filtered out by the enclosing AndSource.
-    limit += start;
 
     MutableInteger leafTerms = new MutableInteger();
-    Predicate<ChangeData> out = rewriteImpl(in, index, limit, leafTerms);
+    Predicate<ChangeData> out = rewriteImpl(in, index, opts, leafTerms);
     if (in == out || out instanceof IndexPredicate) {
-      return new IndexedChangeQuery(index, out, limit);
+      return new IndexedChangeQuery(index, out, opts);
     } else if (out == null /* cannot rewrite */) {
       return in;
     } else {
@@ -153,7 +148,7 @@
    *
    * @param in predicate to rewrite.
    * @param index index whose schema determines which fields are indexed.
-   * @param limit maximum number of results to return.
+   * @param opts other query options.
    * @param leafTerms number of leaf index query terms encountered so far.
    * @return {@code null} if no part of this subtree can be queried in the
    *     index directly. {@code in} if this subtree and all its children can be
@@ -164,7 +159,7 @@
    *     support this predicate.
    */
   private Predicate<ChangeData> rewriteImpl(Predicate<ChangeData> in,
-      ChangeIndex index, int limit, MutableInteger leafTerms)
+      ChangeIndex index, QueryOptions opts, MutableInteger leafTerms)
       throws QueryParseException {
     if (isIndexPredicate(in, index)) {
       if (++leafTerms.value > config.maxTerms()) {
@@ -172,8 +167,10 @@
       }
       return in;
     } else if (in instanceof LimitPredicate) {
-      // Replace any limits with the limit provided by the caller.
-      return new LimitPredicate(limit);
+      // Replace any limits with the limit provided by the caller. The caller
+      // should have already searched the predicate tree for limit predicates
+      // and included that in their limit computation.
+      return new LimitPredicate(opts.limit());
     } else if (!isRewritePossible(in)) {
       return null; // magic to indicate "in" cannot be rewritten
     }
@@ -185,7 +182,7 @@
     List<Predicate<ChangeData>> newChildren = Lists.newArrayListWithCapacity(n);
     for (int i = 0; i < n; i++) {
       Predicate<ChangeData> c = in.getChild(i);
-      Predicate<ChangeData> nc = rewriteImpl(c, index, limit, leafTerms);
+      Predicate<ChangeData> nc = rewriteImpl(c, index, opts, leafTerms);
       if (nc == c) {
         isIndexed.set(i);
         newChildren.add(c);
@@ -205,7 +202,7 @@
     } else if (rewritten.cardinality() == n) {
       return in.copy(newChildren); // All children were rewritten.
     }
-    return partitionChildren(in, newChildren, isIndexed, index, limit);
+    return partitionChildren(in, newChildren, isIndexed, index, opts);
   }
 
   private boolean isIndexPredicate(Predicate<ChangeData> in, ChangeIndex index) {
@@ -221,11 +218,11 @@
       List<Predicate<ChangeData>> newChildren,
       BitSet isIndexed,
       ChangeIndex index,
-      int limit) throws QueryParseException {
+      QueryOptions opts) throws QueryParseException {
     if (isIndexed.cardinality() == 1) {
       int i = isIndexed.nextSetBit(0);
       newChildren.add(
-          0, new IndexedChangeQuery(index, newChildren.remove(i), limit));
+          0, new IndexedChangeQuery(index, newChildren.remove(i), opts));
       return copy(in, newChildren);
     }
 
@@ -245,7 +242,7 @@
         all.add(c);
       }
     }
-    all.add(0, new IndexedChangeQuery(index, in.copy(indexed), limit));
+    all.add(0, new IndexedChangeQuery(index, in.copy(indexed), opts));
     return copy(in, all);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
index 81be0fd..683f8cf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexedChangeQuery.java
@@ -14,15 +14,18 @@
 
 package com.google.gerrit.server.index;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Function;
 import com.google.common.base.MoreObjects;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
+import com.google.common.primitives.Ints;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
 import com.google.gerrit.server.query.change.Paginated;
+import com.google.gerrit.server.query.change.QueryOptions;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 
@@ -40,19 +43,28 @@
  */
 public class IndexedChangeQuery extends Predicate<ChangeData>
     implements ChangeDataSource, Paginated {
+  @VisibleForTesting
+  static QueryOptions convertOptions(QueryOptions opts) {
+    // Increase the limit rather than skipping, since we don't know how many
+    // skipped results would have been filtered out by the enclosing AndSource.
+    int backendLimit = opts.config().maxLimit();
+    int limit = Ints.saturatedCast((long) opts.limit() + opts.start());
+    limit = Math.min(limit, backendLimit);
+    return QueryOptions.create(opts.config(), 0, limit);
+  }
 
   private final ChangeIndex index;
-  private final int limit;
 
+  private QueryOptions opts;
   private Predicate<ChangeData> pred;
   private ChangeDataSource source;
 
   public IndexedChangeQuery(ChangeIndex index, Predicate<ChangeData> pred,
-      int limit) throws QueryParseException {
+      QueryOptions opts) throws QueryParseException {
     this.index = index;
-    this.limit = limit;
+    this.opts = convertOptions(opts);
     this.pred = pred;
-    this.source = index.getSource(pred, 0, limit);
+    this.source = index.getSource(pred, this.opts);
   }
 
   @Override
@@ -74,13 +86,13 @@
   }
 
   @Override
-  public int limit() {
-    return limit;
+  public QueryOptions getOptions() {
+    return opts;
   }
 
   @Override
   public int getCardinality() {
-    return source != null ? source.getCardinality() : limit();
+    return source != null ? source.getCardinality() : opts.limit();
   }
 
   @Override
@@ -126,14 +138,17 @@
 
   @Override
   public ResultSet<ChangeData> restart(int start) throws OrmException {
+    opts = opts.withStart(start);
     try {
-      source = index.getSource(pred, start, limit);
+      source = index.getSource(pred, opts);
     } catch (QueryParseException e) {
       // Don't need to show this exception to the user; the only thing that
       // changed about pred was its start, and any other QPEs that might happen
       // should have already thrown from the constructor.
       throw new OrmException(e);
     }
+    // Don't convert start to a limit, since the caller of this method (see
+    // AndSource) has calculated the actual number to skip.
     return read();
   }
 
@@ -168,14 +183,14 @@
     }
     IndexedChangeQuery o = (IndexedChangeQuery) other;
     return pred.equals(o.pred)
-        && limit == o.limit;
+        && opts.equals(o.opts);
   }
 
   @Override
   public String toString() {
     return MoreObjects.toStringHelper("index")
         .add("p", pred)
-        .add("limit", limit)
+        .add("opts", opts)
         .toString();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index 2c4f30c..da1e7b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -16,7 +16,7 @@
 package com.google.gerrit.server.patch;
 
 import com.google.common.cache.Cache;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
index 6f957da..15277b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -14,15 +14,16 @@
 
 package com.google.gerrit.server.patch;
 
-import static com.google.gerrit.server.ioutil.BasicSerialization.readEnum;
-import static com.google.gerrit.server.ioutil.BasicSerialization.writeEnum;
+import static com.google.common.base.Preconditions.checkState;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.readCanBeNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeCanBeNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
@@ -35,6 +36,16 @@
 public class PatchListKey implements Serializable {
   static final long serialVersionUID = 18L;
 
+  public static final BiMap<Whitespace, Character> WHITESPACE_TYPES = ImmutableBiMap.of(
+      Whitespace.IGNORE_NONE, 'N',
+      Whitespace.IGNORE_TRAILING, 'E',
+      Whitespace.IGNORE_LEADING_AND_TRAILING, 'S',
+      Whitespace.IGNORE_ALL, 'A');
+
+  static {
+    checkState(WHITESPACE_TYPES.size() == Whitespace.values().length);
+  }
+
   private transient ObjectId oldId;
   private transient ObjectId newId;
   private transient Whitespace whitespace;
@@ -108,12 +119,20 @@
   private void writeObject(final ObjectOutputStream out) throws IOException {
     writeCanBeNull(out, oldId);
     writeNotNull(out, newId);
-    writeEnum(out, whitespace);
+    Character c = WHITESPACE_TYPES.get(whitespace);
+    if (c == null) {
+      throw new IOException("Invalid whitespace type: " + whitespace);
+    }
+    out.writeChar(c);
   }
 
   private void readObject(final ObjectInputStream in) throws IOException {
     oldId = readCanBeNull(in);
     newId = readNotNull(in);
-    whitespace = readEnum(in, Whitespace.values());
+    char t = in.readChar();
+    whitespace = WHITESPACE_TYPES.inverse().get(t);
+    if (whitespace == null) {
+      throw new IOException("Invalid whitespace type code: " + t);
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index 0e421db..aa2a8a8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -21,7 +21,7 @@
 import com.google.common.base.Function;
 import com.google.common.base.Throwables;
 import com.google.common.collect.FluentIterable;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
@@ -129,13 +129,13 @@
 
   private static RawTextComparator comparatorFor(Whitespace ws) {
     switch (ws) {
-      case IGNORE_ALL_SPACE:
+      case IGNORE_ALL:
         return RawTextComparator.WS_IGNORE_ALL;
 
-      case IGNORE_SPACE_AT_EOL:
+      case IGNORE_TRAILING:
         return RawTextComparator.WS_IGNORE_TRAILING;
 
-      case IGNORE_SPACE_CHANGE:
+      case IGNORE_LEADING_AND_TRAILING:
         return RawTextComparator.WS_IGNORE_CHANGE;
 
       case IGNORE_NONE:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index ff4496f..d0c3d09 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -17,10 +17,10 @@
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
@@ -65,7 +65,7 @@
   private Project.NameKey projectKey;
   private ObjectReader reader;
   private Change change;
-  private AccountDiffPreference diffPrefs;
+  private DiffPreferencesInfo diffPrefs;
   private boolean againstParent;
   private ObjectId aId;
   private ObjectId bId;
@@ -95,11 +95,11 @@
     this.change = c;
   }
 
-  void setDiffPrefs(final AccountDiffPreference dp) {
+  void setDiffPrefs(final DiffPreferencesInfo dp) {
     diffPrefs = dp;
 
-    context = diffPrefs.getContext();
-    if (context == AccountDiffPreference.WHOLE_FILE_CONTEXT) {
+    context = diffPrefs.context;
+    if (context == DiffPreferencesInfo.WHOLE_FILE_CONTEXT) {
       context = MAX_CONTEXT;
     } else if (context > MAX_CONTEXT) {
       context = MAX_CONTEXT;
@@ -140,12 +140,12 @@
 
     if (!isModify(content)) {
       intralineDifferenceIsPossible = false;
-    } else if (diffPrefs.isIntralineDifference()) {
+    } else if (diffPrefs.intralineDifference) {
       IntraLineDiff d =
           patchListCache.getIntraLineDiff(
               new IntraLineDiffKey(
                 a.id, b.id,
-                diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE),
+                diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE),
               IntraLineDiffArgs.create(
                 a.src, b.src, edits, projectKey, bId, b.path));
       if (d != null) {
@@ -208,7 +208,7 @@
       //
       context = MAX_CONTEXT;
 
-      packContent(diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE);
+      packContent(diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE);
     }
 
     return new PatchScript(change.getKey(), content.getChangeType(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
index 2169671..5836df5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchScriptFactory.java
@@ -18,10 +18,10 @@
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo;
+import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.Patch.ChangeType;
@@ -66,7 +66,7 @@
         String fileName,
         @Assisted("patchSetA") PatchSet.Id patchSetA,
         @Assisted("patchSetB") PatchSet.Id patchSetB,
-        AccountDiffPreference diffPrefs);
+        DiffPreferencesInfo diffPrefs);
   }
 
   private static final Logger log =
@@ -83,7 +83,7 @@
   @Nullable
   private final PatchSet.Id psa;
   private final PatchSet.Id psb;
-  private final AccountDiffPreference diffPrefs;
+  private final DiffPreferencesInfo diffPrefs;
   private final ChangeEditUtil editReader;
   private Optional<ChangeEdit> edit;
 
@@ -110,7 +110,7 @@
       @Assisted final String fileName,
       @Assisted("patchSetA") @Nullable final PatchSet.Id patchSetA,
       @Assisted("patchSetB") final PatchSet.Id patchSetB,
-      @Assisted final AccountDiffPreference diffPrefs) {
+      @Assisted DiffPreferencesInfo diffPrefs) {
     this.repoManager = grm;
     this.builderFactory = builderFactory;
     this.patchListCache = patchListCache;
@@ -156,7 +156,7 @@
 
     try (Repository git = repoManager.openRepository(project)) {
       try {
-        final PatchList list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
+        final PatchList list = listFor(keyFor(diffPrefs.ignoreWhitespace));
         final PatchScriptBuilder b = newBuilder(list, git);
         final PatchListEntry content = list.get(fileName);
 
@@ -192,11 +192,10 @@
   }
 
   private PatchScriptBuilder newBuilder(final PatchList list, Repository git) {
-    final AccountDiffPreference dp = new AccountDiffPreference(diffPrefs);
     final PatchScriptBuilder b = builderFactory.get();
     b.setRepository(git, project);
     b.setChange(change);
-    b.setDiffPrefs(dp);
+    b.setDiffPrefs(diffPrefs);
     b.setTrees(list.isAgainstParent(), list.getOldId(), list.getNewId());
     return b;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java
index 13f8098..2f780bf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java
@@ -60,7 +60,12 @@
     }
     try {
       try (InputStream in = openStream(input)) {
-        loader.installPluginFromStream(name, in);
+        String pluginName = loader.installPluginFromStream(name, in);
+        ListPlugins.PluginInfo info =
+            new ListPlugins.PluginInfo(loader.get(pluginName));
+        return created
+            ? Response.created(info)
+            : Response.ok(info);
       }
     } catch (PluginInstallException e) {
       StringWriter buf = new StringWriter();
@@ -76,9 +81,6 @@
       }
       throw new BadRequestException(buf.toString());
     }
-
-    ListPlugins.PluginInfo info = new ListPlugins.PluginInfo(loader.get(name));
-    return created ? Response.created(info) : Response.ok(info);
   }
 
   private InputStream openStream(Input input)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
index 6eb336d..d94df9c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/JarScanner.java
@@ -102,19 +102,19 @@
         throw new InvalidPluginException("Cannot auto-register", err);
       } catch (RuntimeException err) {
         PluginLoader.log.warn(String.format(
-            "Plugin %s has invaild class file %s inside of %s", pluginName,
+            "Plugin %s has invalid class file %s inside of %s", pluginName,
             entry.getName(), jarFile.getName()), err);
         continue;
       }
 
-      if (def.isConcrete()) {
-        if (!Strings.isNullOrEmpty(def.annotationName)) {
-          rawMap.put(def.annotationName, def);
+      if (!Strings.isNullOrEmpty(def.annotationName)) {
+        if (def.isConcrete()) {
+            rawMap.put(def.annotationName, def);
+        } else {
+          PluginLoader.log.warn(String.format(
+              "Plugin %s tries to @%s(\"%s\") abstract class %s", pluginName,
+              def.annotationName, def.annotationValue, def.className));
         }
-      } else {
-        PluginLoader.log.warn(String.format(
-            "Plugin %s tries to @%s(\"%s\") abstract class %s", pluginName,
-            def.annotationName, def.annotationValue, def.className));
       }
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index 4e651c2..5006401 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -157,7 +157,7 @@
     }
   }
 
-  public void installPluginFromStream(String originalName, InputStream in)
+  public String installPluginFromStream(String originalName, InputStream in)
       throws IOException, PluginInstallException {
     checkRemoteInstall();
 
@@ -197,6 +197,8 @@
 
       cleanInBackground();
     }
+
+    return name;
   }
 
   static Path asTemp(InputStream in, String prefix, String suffix, Path dir)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
index 212bb1c..6cbf7c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ConfigInfo.java
@@ -46,6 +46,7 @@
   public InheritedBooleanInfo createNewChangeForAllNotInTarget;
   public InheritedBooleanInfo requireChangeId;
   public InheritedBooleanInfo enableSignedPush;
+  public InheritedBooleanInfo requireSignedPush;
   public MaxObjectSizeLimitInfo maxObjectSizeLimit;
   public SubmitType submitType;
   public com.google.gerrit.extensions.client.ProjectState state;
@@ -74,6 +75,7 @@
     InheritedBooleanInfo createNewChangeForAllNotInTarget =
         new InheritedBooleanInfo();
     InheritedBooleanInfo enableSignedPush = new InheritedBooleanInfo();
+    InheritedBooleanInfo requireSignedPush = new InheritedBooleanInfo();
 
     useContributorAgreements.value = projectState.isUseContributorAgreements();
     useSignedOffBy.value = projectState.isUseSignedOffBy();
@@ -90,6 +92,7 @@
     createNewChangeForAllNotInTarget.configuredValue =
         p.getCreateNewChangeForAllNotInTarget();
     enableSignedPush.configuredValue = p.getEnableSignedPush();
+    requireSignedPush.configuredValue = p.getRequireSignedPush();
 
     ProjectState parentState = Iterables.getFirst(projectState
         .parents(), null);
@@ -102,6 +105,7 @@
       createNewChangeForAllNotInTarget.inheritedValue =
           parentState.isCreateNewChangeForAllNotInTarget();
       enableSignedPush.inheritedValue = projectState.isEnableSignedPush();
+      requireSignedPush.inheritedValue = projectState.isRequireSignedPush();
     }
 
     this.useContributorAgreements = useContributorAgreements;
@@ -111,6 +115,7 @@
     this.createNewChangeForAllNotInTarget = createNewChangeForAllNotInTarget;
     if (serverEnableSignedPush) {
       this.enableSignedPush = enableSignedPush;
+      this.requireSignedPush = requireSignedPush;
     }
 
     MaxObjectSizeLimitInfo maxObjectSizeLimit = new MaxObjectSizeLimitInfo();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
index 41d4f94..b957ba1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
@@ -106,7 +106,8 @@
         @Override
         public ReflogEntryInfo apply(ReflogEntry e) {
           return new ReflogEntryInfo(e);
-        }});
+        }
+      });
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 455a42b..7094828 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -413,6 +413,15 @@
     });
   }
 
+  public boolean isRequireSignedPush() {
+    return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
+      @Override
+      public InheritableBoolean apply(Project input) {
+        return input.getRequireSignedPush();
+      }
+    });
+  }
+
   public LabelTypes getLabelTypes() {
     Map<String, LabelType> types = Maps.newLinkedHashMap();
     for (ProjectState s : treeInOrder()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
index 724dc45..76ad2f0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutConfig.java
@@ -70,6 +70,7 @@
     public InheritableBoolean createNewChangeForAllNotInTarget;
     public InheritableBoolean requireChangeId;
     public InheritableBoolean enableSignedPush;
+    public InheritableBoolean requireSignedPush;
     public String maxObjectSizeLimit;
     public SubmitType submitType;
     public com.google.gerrit.extensions.client.ProjectState state;
@@ -166,8 +167,13 @@
         p.setRequireChangeID(input.requireChangeId);
       }
 
-      if (input.enableSignedPush != null) {
-        p.setEnableSignedPush(input.enableSignedPush);
+      if (serverEnableSignedPush) {
+        if (input.enableSignedPush != null) {
+          p.setEnableSignedPush(input.enableSignedPush);
+        }
+        if (input.requireSignedPush != null) {
+          p.setRequireSignedPush(input.requireSignedPush);
+        }
       }
 
       if (input.maxObjectSizeLimit != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
index f48cfd8..9ed0447 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
@@ -128,7 +128,7 @@
       // limit the caller wants.  Restart the source and continue.
       //
       Paginated p = (Paginated) source;
-      while (skipped && r.size() < p.limit() + start) {
+      while (skipped && r.size() < p.getOptions().limit() + start) {
         skipped = false;
         ResultSet<ChangeData> next = p.restart(nextStart);
 
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 f52edc1..2fdeea4 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
@@ -47,6 +47,7 @@
 import com.google.gerrit.server.index.ChangeIndex;
 import com.google.gerrit.server.index.FieldDef;
 import com.google.gerrit.server.index.IndexCollection;
+import com.google.gerrit.server.index.IndexRewriter;
 import com.google.gerrit.server.index.Schema;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.ChangeControl;
@@ -143,7 +144,7 @@
   public static class Arguments {
     final Provider<ReviewDb> db;
     final Provider<InternalChangeQuery> queryProvider;
-    final Provider<ChangeQueryRewriter> rewriter;
+    final IndexRewriter rewriter;
     final IdentifiedUser.GenericFactory userFactory;
     final CapabilityControl.Factory capabilityControlFactory;
     final ChangeControl.GenericFactory changeControlGenericFactory;
@@ -170,7 +171,7 @@
     @VisibleForTesting
     public Arguments(Provider<ReviewDb> db,
         Provider<InternalChangeQuery> queryProvider,
-        Provider<ChangeQueryRewriter> rewriter,
+        IndexRewriter rewriter,
         IdentifiedUser.GenericFactory userFactory,
         Provider<CurrentUser> self,
         CapabilityControl.Factory capabilityControlFactory,
@@ -204,7 +205,7 @@
     private Arguments(
         Provider<ReviewDb> db,
         Provider<InternalChangeQuery> queryProvider,
-        Provider<ChangeQueryRewriter> rewriter,
+        IndexRewriter rewriter,
         IdentifiedUser.GenericFactory userFactory,
         Provider<CurrentUser> self,
         CapabilityControl.Factory capabilityControlFactory,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
deleted file mode 100644
index 83492d2..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
-
-public interface ChangeQueryRewriter {
-  Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start, int limit)
-      throws QueryParseException;
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
index e829e53..dd3c3b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommentPredicate.java
@@ -32,9 +32,11 @@
   @Override
   public boolean match(ChangeData object) throws OrmException {
     try {
-      for (ChangeData cData : index.getSource(
-          Predicate.and(new LegacyChangeIdPredicate(
-              index.getSchema(), object.getId()), this), 0, 1).read()) {
+      Predicate<ChangeData> p = Predicate.and(
+          new LegacyChangeIdPredicate(index.getSchema(), object.getId()),
+          this);
+      for (ChangeData cData
+          : index.getSource(p, QueryOptions.oneResult()).read()) {
         if (cData.getId().equals(object.getId())) {
           return true;
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index ea8a3ef..9b47302 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.CodeReviewCommit;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
-import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.git.IntegrationException;
 import com.google.gerrit.server.git.strategy.SubmitStrategy;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
@@ -129,7 +129,8 @@
             conflicts = !strategy.dryRun(commit, otherCommit);
             args.conflictsCache.put(conflictsKey, conflicts);
             return conflicts;
-          } catch (MergeException | NoSuchProjectException | IOException e) {
+          } catch (IntegrationException | NoSuchProjectException
+              | IOException e) {
             throw new IllegalStateException(e);
           }
         }
@@ -148,7 +149,7 @@
         }
 
         private Set<RevCommit> getAlreadyAccepted(Repository repo, RevWalk rw,
-            CodeReviewCommit tip) throws MergeException {
+            CodeReviewCommit tip) throws IntegrationException {
           Set<RevCommit> alreadyAccepted = Sets.newHashSet();
 
           if (tip != null) {
@@ -164,7 +165,7 @@
               }
             }
           } catch (IOException e) {
-            throw new MergeException(
+            throw new IntegrationException(
                 "Failed to determine already accepted commits.", e);
           }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
index 5655154..5b9b94c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
@@ -58,7 +58,7 @@
         Predicate<ChangeData> thisId =
             new LegacyChangeIdPredicate(index.getSchema(), cd.getId());
         Iterable<ChangeData> results =
-            index.getSource(and(thisId, this), 0, 1).read();
+            index.getSource(and(thisId, this), QueryOptions.oneResult()).read();
         return !Iterables.isEmpty(results);
       } catch (QueryParseException e) {
         throw new OrmException(e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index aa977dd..a2ccc3a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.gerrit.server.index.ChangeField.SUBMISSIONID;
 import static com.google.gerrit.server.query.Predicate.and;
 import static com.google.gerrit.server.query.Predicate.not;
@@ -215,6 +216,13 @@
     return query(commit(schema(indexes), id.name()));
   }
 
+  public List<ChangeData> byProjectCommits(Project.NameKey project,
+      List<String> hashes) throws OrmException {
+    int n = indexConfig.maxTerms() - 1;
+    checkArgument(hashes.size() <= n, "cannot exceed %s commits", n);
+    return query(and(project(project), or(commits(schema(indexes), hashes))));
+  }
+
   public List<ChangeData> bySubmissionId(String cs) throws OrmException {
     if (Strings.isNullOrEmpty(cs) || !schema(indexes).hasField(SUBMISSIONID)) {
       return Collections.emptyList();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
index 72230f6..cf3140a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -36,9 +36,11 @@
   @Override
   public boolean match(ChangeData object) throws OrmException {
     try {
-      for (ChangeData cData : index.getSource(
-          Predicate.and(new LegacyChangeIdPredicate(
-              index.getSchema(), object.getId()), this), 0, 1).read()) {
+      Predicate<ChangeData> p = Predicate.and(
+          new LegacyChangeIdPredicate(index.getSchema(), object.getId()),
+          this);
+      for (ChangeData cData
+          : index.getSource(p, QueryOptions.oneResult()).read()) {
         if (cData.getId().equals(object.getId())) {
           return true;
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
index b84cc79..b068030 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OutputStreamQuery.java
@@ -14,24 +14,31 @@
 
 package com.google.gerrit.server.query.change;
 
+import static com.google.common.base.Preconditions.checkState;
 import static java.nio.charset.StandardCharsets.UTF_8;
 
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.PatchSetAttribute;
 import com.google.gerrit.server.data.QueryStatsAttribute;
 import com.google.gerrit.server.events.EventFactory;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.SubmitRuleEvaluator;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gson.Gson;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
 import org.joda.time.format.DateTimeFormat;
 import org.joda.time.format.DateTimeFormatter;
@@ -47,7 +54,9 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Change query implementation that outputs to a stream in the style of an SSH
@@ -64,6 +73,8 @@
     TEXT, JSON
   }
 
+  private final Provider<ReviewDb> db;
+  private final GitRepositoryManager repoManager;
   private final ChangeQueryBuilder queryBuilder;
   private final QueryProcessor queryProcessor;
   private final EventFactory eventFactory;
@@ -86,11 +97,15 @@
 
   @Inject
   OutputStreamQuery(
+      Provider<ReviewDb> db,
+      GitRepositoryManager repoManager,
       ChangeQueryBuilder queryBuilder,
       QueryProcessor queryProcessor,
       EventFactory eventFactory,
       TrackingFooters trackingFooters,
       CurrentUser user) {
+    this.db = db;
+    this.repoManager = repoManager;
     this.queryBuilder = queryBuilder;
     this.queryProcessor = queryProcessor;
     this.eventFactory = eventFactory;
@@ -179,76 +194,16 @@
         final QueryStatsAttribute stats = new QueryStatsAttribute();
         stats.runTimeMilliseconds = TimeUtil.nowMs();
 
+        Map<Project.NameKey, Repository> repos = new HashMap<>();
+        Map<Project.NameKey, RevWalk> revWalks = new HashMap<>();
         QueryResult results =
             queryProcessor.queryChanges(queryBuilder.parse(queryString));
-        ChangeAttribute c = null;
-        for (ChangeData d : results.changes()) {
-          ChangeControl cc = d.changeControl().forUser(user);
-
-          LabelTypes labelTypes = cc.getLabelTypes();
-          c = eventFactory.asChangeAttribute(d.change());
-          eventFactory.extend(c, d.change());
-
-          if (!trackingFooters.isEmpty()) {
-            eventFactory.addTrackingIds(c,
-                trackingFooters.extract(d.commitFooters()));
+        try {
+          for (ChangeData d : results.changes()) {
+            show(buildChangeAttribute(d, repos, revWalks));
           }
-
-          if (includeAllReviewers) {
-            eventFactory.addAllReviewers(c, d.notes());
-          }
-
-          if (includeSubmitRecords) {
-            eventFactory.addSubmitRecords(c, new SubmitRuleEvaluator(d)
-                .setAllowClosed(true)
-                .setAllowDraft(true)
-                .evaluate());
-          }
-
-          if (includeCommitMessage) {
-            eventFactory.addCommitMessage(c, d.commitMessage());
-          }
-
-          if (includePatchSets) {
-            if (includeFiles) {
-              eventFactory.addPatchSets(c, d.patchSets(),
-                includeApprovals ? d.approvals().asMap() : null,
-                includeFiles, d.change(), labelTypes);
-            } else {
-              eventFactory.addPatchSets(c, d.patchSets(),
-                  includeApprovals ? d.approvals().asMap() : null,
-                  labelTypes);
-            }
-          }
-
-          if (includeCurrentPatchSet) {
-            PatchSet current = d.currentPatchSet();
-            if (current != null) {
-              c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
-              eventFactory.addApprovals(c.currentPatchSet,
-                  d.currentApprovals(), labelTypes);
-
-              if (includeFiles) {
-                eventFactory.addPatchSetFileNames(c.currentPatchSet,
-                    d.change(), d.currentPatchSet());
-              }
-            }
-          }
-
-          if (includeComments) {
-            eventFactory.addComments(c, d.messages());
-            if (includePatchSets) {
-              for (PatchSetAttribute attribute : c.patchSets) {
-                eventFactory.addPatchSetComments(attribute,  d.publishedComments());
-              }
-            }
-          }
-
-          if (includeDependencies) {
-            eventFactory.addDependencies(c, d.change());
-          }
-
-          show(c);
+        } finally {
+          closeAll(revWalks.values(), repos.values());
         }
 
         stats.rowCount = results.changes().size();
@@ -277,6 +232,107 @@
     }
   }
 
+  private ChangeAttribute buildChangeAttribute(ChangeData d,
+      Map<Project.NameKey, Repository> repos,
+      Map<Project.NameKey, RevWalk> revWalks)
+      throws OrmException, IOException {
+    ChangeControl cc = d.changeControl().forUser(user);
+
+    LabelTypes labelTypes = cc.getLabelTypes();
+    ChangeAttribute c = eventFactory.asChangeAttribute(db.get(), d.change());
+    eventFactory.extend(c, d.change());
+
+    if (!trackingFooters.isEmpty()) {
+      eventFactory.addTrackingIds(c,
+          trackingFooters.extract(d.commitFooters()));
+    }
+
+    if (includeAllReviewers) {
+      eventFactory.addAllReviewers(db.get(), c, d.notes());
+    }
+
+    if (includeSubmitRecords) {
+      eventFactory.addSubmitRecords(c, new SubmitRuleEvaluator(d)
+          .setAllowClosed(true)
+          .setAllowDraft(true)
+          .evaluate());
+    }
+
+    if (includeCommitMessage) {
+      eventFactory.addCommitMessage(c, d.commitMessage());
+    }
+
+    RevWalk rw = null;
+    if (includePatchSets || includeCurrentPatchSet || includeDependencies) {
+      Project.NameKey p = d.change().getProject();
+      rw = revWalks.get(p);
+      // Cache and reuse repos and revwalks.
+      if (rw == null) {
+        Repository repo = repoManager.openRepository(p);
+        checkState(repos.put(p, repo) == null);
+        rw = new RevWalk(repo);
+        revWalks.put(p, rw);
+      }
+    }
+
+    if (includePatchSets) {
+      if (includeFiles) {
+        eventFactory.addPatchSets(db.get(), rw, c, d.patchSets(),
+          includeApprovals ? d.approvals().asMap() : null,
+          includeFiles, d.change(), labelTypes);
+      } else {
+        eventFactory.addPatchSets(db.get(), rw, c, d.patchSets(),
+            includeApprovals ? d.approvals().asMap() : null,
+            labelTypes);
+      }
+    }
+
+    if (includeCurrentPatchSet) {
+      PatchSet current = d.currentPatchSet();
+      if (current != null) {
+        c.currentPatchSet =
+            eventFactory.asPatchSetAttribute(db.get(), rw, current);
+        eventFactory.addApprovals(c.currentPatchSet,
+            d.currentApprovals(), labelTypes);
+
+        if (includeFiles) {
+          eventFactory.addPatchSetFileNames(c.currentPatchSet,
+              d.change(), d.currentPatchSet());
+        }
+      }
+    }
+
+    if (includeComments) {
+      eventFactory.addComments(c, d.messages());
+      if (includePatchSets) {
+        for (PatchSetAttribute attribute : c.patchSets) {
+          eventFactory.addPatchSetComments(
+              attribute, d.publishedComments());
+        }
+      }
+    }
+
+    if (includeDependencies) {
+      eventFactory.addDependencies(rw, c, d.change(), d.currentPatchSet());
+    }
+
+    return c;
+  }
+
+  private static void closeAll(Iterable<RevWalk> revWalks,
+      Iterable<Repository> repos) {
+    if (repos != null) {
+      for (Repository repo : repos) {
+        repo.close();
+      }
+    }
+    if (revWalks != null) {
+      for (RevWalk revWalk : revWalks) {
+        revWalk.close();
+      }
+    }
+  }
+
   private void show(Object data) {
     switch (outputFormat) {
       default:
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
index 7afd934..3278b7f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
@@ -18,7 +18,7 @@
 import com.google.gwtorm.server.ResultSet;
 
 public interface Paginated {
-  int limit();
+  QueryOptions getOptions();
 
   ResultSet<ChangeData> restart(int start) throws OrmException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryOptions.java
new file mode 100644
index 0000000..1964fa5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryOptions.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2015 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.auto.value.AutoValue;
+import com.google.gerrit.server.index.IndexConfig;
+
+@AutoValue
+public abstract class QueryOptions {
+  public static QueryOptions create(IndexConfig config, int start, int limit) {
+    checkArgument(start >= 0, "start must be nonnegative: %s", start);
+    checkArgument(limit > 0, "limit must be positive: %s", limit);
+    return new AutoValue_QueryOptions(config, start, limit);
+  }
+
+  public static QueryOptions oneResult() {
+    return create(IndexConfig.createDefault(), 0, 1);
+  }
+
+  public abstract IndexConfig config();
+  public abstract int start();
+  public abstract int limit();
+
+  public QueryOptions withLimit(int newLimit) {
+    return create(config(), start(), newLimit);
+  }
+
+  public QueryOptions withStart(int newStart) {
+    return create(config(), newStart, limit());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index 6068dd0..a2c8b81 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.CurrentUser;
 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.project.ChangeControl;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
@@ -39,7 +40,7 @@
   private final Provider<ReviewDb> db;
   private final Provider<CurrentUser> userProvider;
   private final ChangeControl.GenericFactory changeControlFactory;
-  private final ChangeQueryRewriter queryRewriter;
+  private final IndexRewriter rewriter;
   private final IndexConfig indexConfig;
 
   private int limitFromCaller;
@@ -50,12 +51,12 @@
   QueryProcessor(Provider<ReviewDb> db,
       Provider<CurrentUser> userProvider,
       ChangeControl.GenericFactory changeControlFactory,
-      ChangeQueryRewriter queryRewriter,
+      IndexRewriter rewriter,
       IndexConfig indexConfig) {
     this.db = db;
     this.userProvider = userProvider;
     this.changeControlFactory = changeControlFactory;
-    this.queryRewriter = queryRewriter;
+    this.rewriter = rewriter;
     this.indexConfig = indexConfig;
   }
 
@@ -139,10 +140,11 @@
             "Cannot go beyond page " + indexConfig.maxPages() + "of results");
       }
 
-      Predicate<ChangeData> s = queryRewriter.rewrite(q, start, limit + 1);
+      QueryOptions opts = QueryOptions.create(indexConfig, start, limit + 1);
+      Predicate<ChangeData> s = rewriter.rewrite(q, opts);
       if (!(s instanceof ChangeDataSource)) {
         q = Predicate.and(open(), q);
-        s = queryRewriter.rewrite(q, start, limit);
+        s = rewriter.rewrite(q, opts);
       }
       if (!(s instanceof ChangeDataSource)) {
         throw new QueryParseException("invalid query: " + s);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
index 8da9e2a..148d1df 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -76,6 +77,7 @@
         for (Class<?> c : new Class<?>[] {
             AllProjectsName.class,
             AllUsersCreator.class,
+            AllUsersName.class,
             GitRepositoryManager.class,
             SitePaths.class,
             }) {
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 a4a7ddc..1c29988 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
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_113> C = Schema_113.class;
+  public static final Class<Schema_114> C = Schema_114.class;
 
   public static int getBinaryVersion() {
     return guessVersion(C);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_114.java
similarity index 62%
copy from gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_114.java
index 02bc8dc..85c93d2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeConflictException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_114.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2015 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.
@@ -12,12 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.git;
+package com.google.gerrit.server.schema;
 
-/** Indicates that the commit cannot be merged without conflicts. */
-public class MergeConflictException extends Exception {
-  private static final long serialVersionUID = 1L;
-  public MergeConflictException(String msg) {
-    super(msg, null);
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_114 extends SchemaVersion {
+  @Inject
+  Schema_114(Provider<Schema_113> prior) {
+    super(prior);
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
index 1e752d4..f5e6d74 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/CommentsTest.java
@@ -401,7 +401,8 @@
       @Override
       public ResultSet<PatchLineComment> answer() throws Throwable {
         return new ListResultSet<>(Lists.newArrayList(comments));
-      }};
+      }
+    };
   }
 
   private void assertListComments(RevisionResource res,
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
index ab00ba8..4f409d1 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
@@ -98,7 +98,7 @@
     assertThat(cfg.getString(SECT, SUB, "sd")).isNull();
 
     SectionInfo out = new SectionInfo();
-    ConfigUtil.loadSection(cfg, SECT, SUB, out, d);
+    ConfigUtil.loadSection(cfg, SECT, SUB, out, d, null);
     assertThat(out.i).isEqualTo(in.i);
     assertThat(out.ii).isEqualTo(in.ii);
     assertThat(out.id).isEqualTo(d.id);
@@ -115,6 +115,25 @@
   }
 
   @Test
+  public void mergeSection() throws Exception {
+    SectionInfo d = SectionInfo.defaults();
+    Config cfg = new Config();
+    ConfigUtil.storeSection(cfg, SECT, SUB, d, d);
+
+    SectionInfo in = new SectionInfo();
+    in.i = 42;
+
+    SectionInfo out = new SectionInfo();
+    ConfigUtil.loadSection(cfg, SECT, SUB, out, d, in);
+    // Check original values preserved
+    assertThat(out.id).isEqualTo(d.id);
+    // Check merged values
+    assertThat(out.i).isEqualTo(in.i);
+    // Check that boolean attribute not nullified
+    assertThat(out.bb).isFalse();
+  }
+
+  @Test
   public void testTimeUnit() {
     assertEquals(ms(0, MILLISECONDS), parse("0"));
     assertEquals(ms(2, MILLISECONDS), parse("2ms"));
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
index 9b3d5ed..d38da43 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/FakeIndex.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.QueryOptions;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 
@@ -84,8 +85,8 @@
   }
 
   @Override
-  public ChangeDataSource getSource(Predicate<ChangeData> p, int start,
-      int limit) throws QueryParseException {
+  public ChangeDataSource getSource(Predicate<ChangeData> p, QueryOptions opts)
+      throws QueryParseException {
     return new FakeIndex.Source(p);
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
deleted file mode 100644
index 128c2a7..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
+++ /dev/null
@@ -1,261 +0,0 @@
-// Copyright (C) 2013 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.index;
-
-import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT;
-import static com.google.gerrit.reviewdb.client.Change.Status.ABANDONED;
-import static com.google.gerrit.reviewdb.client.Change.Status.DRAFT;
-import static com.google.gerrit.reviewdb.client.Change.Status.MERGED;
-import static com.google.gerrit.reviewdb.client.Change.Status.NEW;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertSame;
-import static org.junit.Assert.assertTrue;
-
-import com.google.common.collect.ImmutableList;
-import com.google.gerrit.reviewdb.client.Change;
-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.AndSource;
-import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.OrSource;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-import java.util.Arrays;
-import java.util.EnumSet;
-import java.util.Set;
-
-public class IndexRewriteTest {
-  @Rule
-  public ExpectedException exception = ExpectedException.none();
-
-  private FakeIndex index;
-  private IndexCollection indexes;
-  private ChangeQueryBuilder queryBuilder;
-  private IndexRewriteImpl rewrite;
-
-  @Before
-  public void setUp() throws Exception {
-    index = new FakeIndex(FakeIndex.V2);
-    indexes = new IndexCollection();
-    indexes.setSearchIndex(index);
-    queryBuilder = new FakeQueryBuilder(indexes);
-    rewrite = new IndexRewriteImpl(indexes,
-        IndexConfig.create(0, 0, 3, 100));
-  }
-
-  @Test
-  public void testIndexPredicate() throws Exception {
-    Predicate<ChangeData> in = parse("file:a");
-    assertEquals(query(in), rewrite(in));
-  }
-
-  @Test
-  public void testNonIndexPredicate() throws Exception {
-    Predicate<ChangeData> in = parse("foo:a");
-    assertSame(in, rewrite(in));
-  }
-
-  @Test
-  public void testIndexPredicates() throws Exception {
-    Predicate<ChangeData> in = parse("file:a file:b");
-    assertEquals(query(in), rewrite(in));
-  }
-
-  @Test
-  public void testNonIndexPredicates() throws Exception {
-    Predicate<ChangeData> in = parse("foo:a OR foo:b");
-    assertEquals(in, rewrite(in));
-  }
-
-  @Test
-  public void testOneIndexPredicate() throws Exception {
-    Predicate<ChangeData> in = parse("foo:a file:b");
-    Predicate<ChangeData> out = rewrite(in);
-    assertSame(AndSource.class, out.getClass());
-    assertEquals(
-        ImmutableList.of(query(in.getChild(1)), in.getChild(0)),
-        out.getChildren());
-  }
-
-  @Test
-  public void testThreeLevelTreeWithAllIndexPredicates() throws Exception {
-    Predicate<ChangeData> in =
-        parse("-status:abandoned (file:a OR file:b)");
-    assertEquals(
-        query(in),
-        rewrite.rewrite(in, 0, DEFAULT_MAX_QUERY_LIMIT));
-  }
-
-  @Test
-  public void testThreeLevelTreeWithSomeIndexPredicates() throws Exception {
-    Predicate<ChangeData> in = parse("-foo:a (file:b OR file:c)");
-    Predicate<ChangeData> out = rewrite(in);
-    assertEquals(AndSource.class, out.getClass());
-    assertEquals(
-        ImmutableList.of(query(in.getChild(1)), in.getChild(0)),
-        out.getChildren());
-  }
-
-  @Test
-  public void testMultipleIndexPredicates() throws Exception {
-    Predicate<ChangeData> in =
-        parse("file:a OR foo:b OR file:c OR foo:d");
-    Predicate<ChangeData> out = rewrite(in);
-    assertSame(OrSource.class, out.getClass());
-    assertEquals(ImmutableList.of(
-          query(Predicate.or(in.getChild(0), in.getChild(2))),
-          in.getChild(1), in.getChild(3)),
-        out.getChildren());
-  }
-
-  @Test
-  public void testIndexAndNonIndexPredicates() throws Exception {
-    Predicate<ChangeData> in = parse("status:new bar:p file:a");
-    Predicate<ChangeData> out = rewrite(in);
-    assertSame(AndSource.class, out.getClass());
-    assertEquals(ImmutableList.of(
-          query(Predicate.and(in.getChild(0), in.getChild(2))),
-          in.getChild(1)),
-        out.getChildren());
-  }
-
-  @Test
-  public void testDuplicateCompoundNonIndexOnlyPredicates() throws Exception {
-    Predicate<ChangeData> in =
-        parse("(status:new OR status:draft) bar:p file:a");
-    Predicate<ChangeData> out = rewrite(in);
-    assertSame(AndSource.class, out.getClass());
-    assertEquals(ImmutableList.of(
-          query(Predicate.and(in.getChild(0), in.getChild(2))),
-          in.getChild(1)),
-        out.getChildren());
-  }
-
-  @Test
-  public void testDuplicateCompoundIndexOnlyPredicates() throws Exception {
-    Predicate<ChangeData> in =
-        parse("(status:new OR file:a) bar:p file:b");
-    Predicate<ChangeData> out = rewrite(in);
-    assertSame(AndSource.class, out.getClass());
-    assertEquals(ImmutableList.of(
-          query(Predicate.and(in.getChild(0), in.getChild(2))),
-          in.getChild(1)),
-        out.getChildren());
-  }
-
-  @Test
-  public void testLimitArgumentOverridesAllLimitPredicates() throws Exception {
-    Predicate<ChangeData> in = parse("limit:1 file:a limit:3");
-    Predicate<ChangeData> out = rewrite(in, 5);
-    assertSame(AndSource.class, out.getClass());
-    assertEquals(ImmutableList.of(
-          query(in.getChild(1), 5),
-          parse("limit:5"),
-          parse("limit:5")),
-        out.getChildren());
-  }
-
-  @Test
-  public void testStartIncreasesLimit() throws Exception {
-    int n = 3;
-    Predicate<ChangeData> f = parse("file:a");
-    Predicate<ChangeData> l = parse("limit:" + n);
-    Predicate<ChangeData> in = and(f, l);
-    assertEquals(and(query(f, 3), parse("limit:3")), rewrite.rewrite(in, 0, n));
-    assertEquals(and(query(f, 4), parse("limit:4")), rewrite.rewrite(in, 1, n));
-    assertEquals(and(query(f, 5), parse("limit:5")), rewrite.rewrite(in, 2, n));
-  }
-
-  @Test
-  public void testGetPossibleStatus() throws Exception {
-    assertEquals(EnumSet.allOf(Change.Status.class), status("file:a"));
-    assertEquals(EnumSet.of(NEW), status("is:new"));
-    assertEquals(EnumSet.of(DRAFT, MERGED, ABANDONED),
-        status("-is:new"));
-    assertEquals(EnumSet.of(NEW, MERGED), status("is:new OR is:merged"));
-
-    EnumSet<Change.Status> none = EnumSet.noneOf(Change.Status.class);
-    assertEquals(none, status("is:new is:merged"));
-    assertEquals(none, status("(is:new is:draft) (is:merged)"));
-    assertEquals(none, status("(is:new is:draft) (is:merged)"));
-
-    assertEquals(EnumSet.of(MERGED),
-        status("(is:new is:draft) OR (is:merged)"));
-  }
-
-  @Test
-  public void testUnsupportedIndexOperator() throws Exception {
-    Predicate<ChangeData> in = parse("status:merged file:a");
-    assertEquals(query(in), rewrite(in));
-
-    indexes.setSearchIndex(new FakeIndex(FakeIndex.V1));
-    Predicate<ChangeData> out = rewrite(in);
-    assertTrue(out instanceof AndPredicate);
-    assertEquals(ImmutableList.of(
-          query(in.getChild(0)),
-          in.getChild(1)),
-        out.getChildren());
-  }
-
-  @Test
-  public void testTooManyTerms() throws Exception {
-    String q = "file:a OR file:b OR file:c";
-    Predicate<ChangeData> in = parse(q);
-    assertEquals(query(in), rewrite(in));
-
-    exception.expect(QueryParseException.class);
-    exception.expectMessage("too many terms in query");
-    rewrite(parse(q + " OR file:d"));
-  }
-
-  private Predicate<ChangeData> parse(String query) throws QueryParseException {
-    return queryBuilder.parse(query);
-  }
-
-  @SafeVarargs
-  private static AndSource and(Predicate<ChangeData>... preds) {
-    return new AndSource(Arrays.asList(preds));
-  }
-
-  private Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
-      throws QueryParseException {
-    return rewrite.rewrite(in, 0, DEFAULT_MAX_QUERY_LIMIT);
-  }
-
-  private Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int limit)
-      throws QueryParseException {
-    return rewrite.rewrite(in, 0, limit);
-  }
-
-  private IndexedChangeQuery query(Predicate<ChangeData> p)
-      throws QueryParseException {
-    return query(p, DEFAULT_MAX_QUERY_LIMIT);
-  }
-
-  private IndexedChangeQuery query(Predicate<ChangeData> p, int limit)
-      throws QueryParseException {
-    return new IndexedChangeQuery(index, p, limit);
-  }
-
-  private Set<Change.Status> status(String query) throws QueryParseException {
-    return IndexRewriteImpl.getPossibleStatus(parse(query));
-  }
-}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriterTest.java
new file mode 100644
index 0000000..9ac83d5
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriterTest.java
@@ -0,0 +1,298 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.index;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT;
+import static com.google.gerrit.reviewdb.client.Change.Status.ABANDONED;
+import static com.google.gerrit.reviewdb.client.Change.Status.DRAFT;
+import static com.google.gerrit.reviewdb.client.Change.Status.MERGED;
+import static com.google.gerrit.reviewdb.client.Change.Status.NEW;
+import static com.google.gerrit.server.index.IndexedChangeQuery.convertOptions;
+import static com.google.gerrit.server.query.Predicate.and;
+import static com.google.gerrit.server.query.Predicate.or;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.reviewdb.client.Change;
+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.AndSource;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.OrSource;
+import com.google.gerrit.server.query.change.QueryOptions;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.Set;
+
+public class IndexRewriterTest {
+  private static final IndexConfig CONFIG = IndexConfig.createDefault();
+
+  @Rule
+  public ExpectedException exception = ExpectedException.none();
+
+  private FakeIndex index;
+  private IndexCollection indexes;
+  private ChangeQueryBuilder queryBuilder;
+  private IndexRewriter rewrite;
+
+  @Before
+  public void setUp() throws Exception {
+    index = new FakeIndex(FakeIndex.V2);
+    indexes = new IndexCollection();
+    indexes.setSearchIndex(index);
+    queryBuilder = new FakeQueryBuilder(indexes);
+    rewrite = new IndexRewriter(indexes,
+        IndexConfig.create(0, 0, 3, 100));
+  }
+
+  @Test
+  public void testIndexPredicate() throws Exception {
+    Predicate<ChangeData> in = parse("file:a");
+    assertThat(rewrite(in)).isEqualTo(query(in));
+  }
+
+  @Test
+  public void testNonIndexPredicate() throws Exception {
+    Predicate<ChangeData> in = parse("foo:a");
+    assertThat(in).isSameAs(rewrite(in));
+  }
+
+  @Test
+  public void testIndexPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("file:a file:b");
+    assertThat(rewrite(in)).isEqualTo(query(in));
+  }
+
+  @Test
+  public void testNonIndexPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("foo:a OR foo:b");
+    assertThat(in).isEqualTo(rewrite(in));
+  }
+
+  @Test
+  public void testOneIndexPredicate() throws Exception {
+    Predicate<ChangeData> in = parse("foo:a file:b");
+    Predicate<ChangeData> out = rewrite(in);
+    assertThat(AndSource.class).isSameAs(out.getClass());
+    assertThat(out.getChildren())
+        .containsExactly(
+            query(in.getChild(1)),
+            in.getChild(0))
+        .inOrder();
+  }
+
+  @Test
+  public void testThreeLevelTreeWithAllIndexPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("-status:abandoned (file:a OR file:b)");
+    assertThat(rewrite.rewrite(in, options(0, DEFAULT_MAX_QUERY_LIMIT)))
+        .isEqualTo(query(in));
+  }
+
+  @Test
+  public void testThreeLevelTreeWithSomeIndexPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("-foo:a (file:b OR file:c)");
+    Predicate<ChangeData> out = rewrite(in);
+    assertThat(out.getClass()).isSameAs(AndSource.class);
+    assertThat(out.getChildren())
+        .containsExactly(
+          query(in.getChild(1)),
+          in.getChild(0))
+        .inOrder();
+  }
+
+  @Test
+  public void testMultipleIndexPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("file:a OR foo:b OR file:c OR foo:d");
+    Predicate<ChangeData> out = rewrite(in);
+    assertThat(out.getClass()).isSameAs(OrSource.class);
+    assertThat(out.getChildren())
+        .containsExactly(
+          query(or(in.getChild(0), in.getChild(2))),
+          in.getChild(1),
+          in.getChild(3))
+        .inOrder();
+  }
+
+  @Test
+  public void testIndexAndNonIndexPredicates() throws Exception {
+    Predicate<ChangeData> in = parse("status:new bar:p file:a");
+    Predicate<ChangeData> out = rewrite(in);
+    assertThat(AndSource.class).isSameAs(out.getClass());
+    assertThat(out.getChildren())
+        .containsExactly(
+          query(and(in.getChild(0), in.getChild(2))),
+          in.getChild(1))
+        .inOrder();
+  }
+
+  @Test
+  public void testDuplicateCompoundNonIndexOnlyPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("(status:new OR status:draft) bar:p file:a");
+    Predicate<ChangeData> out = rewrite(in);
+    assertThat(out.getClass()).isEqualTo(AndSource.class);
+    assertThat(out.getChildren())
+        .containsExactly(
+          query(and(in.getChild(0), in.getChild(2))),
+          in.getChild(1))
+        .inOrder();
+  }
+
+  @Test
+  public void testDuplicateCompoundIndexOnlyPredicates() throws Exception {
+    Predicate<ChangeData> in =
+        parse("(status:new OR file:a) bar:p file:b");
+    Predicate<ChangeData> out = rewrite(in);
+    assertThat(out.getClass()).isEqualTo(AndSource.class);
+    assertThat(out.getChildren())
+        .containsExactly(
+          query(and(in.getChild(0), in.getChild(2))),
+          in.getChild(1))
+        .inOrder();
+  }
+
+  @Test
+  public void testOptionsArgumentOverridesAllLimitPredicates()
+      throws Exception {
+    Predicate<ChangeData> in = parse("limit:1 file:a limit:3");
+    Predicate<ChangeData> out = rewrite(in, options(0, 5));
+    assertThat(out.getClass()).isEqualTo(AndSource.class);
+    assertThat(out.getChildren())
+        .containsExactly(
+          query(in.getChild(1), 5),
+          parse("limit:5"),
+          parse("limit:5"))
+        .inOrder();
+  }
+
+  @Test
+  public void testStartIncreasesLimitInQueryButNotPredicate() throws Exception {
+    int n = 3;
+    Predicate<ChangeData> f = parse("file:a");
+    Predicate<ChangeData> l = parse("limit:" + n);
+    Predicate<ChangeData> in = andSource(f, l);
+    assertThat(rewrite.rewrite(in, options(0, n)))
+        .isEqualTo(andSource(query(f, 3), l));
+    assertThat(rewrite.rewrite(in, options(1, n)))
+        .isEqualTo(andSource(query(f, 4), l));
+    assertThat(rewrite.rewrite(in, options(2, n)))
+        .isEqualTo(andSource(query(f, 5), l));
+  }
+
+  @Test
+  public void testGetPossibleStatus() throws Exception {
+    assertThat(status("file:a")).isEqualTo(EnumSet.allOf(Change.Status.class));
+    assertThat(status("is:new")).containsExactly(NEW);
+    assertThat(status("-is:new"))
+        .containsExactly(DRAFT, MERGED, ABANDONED);
+    assertThat(status("is:new OR is:merged")).containsExactly(NEW, MERGED);
+
+    assertThat(status("is:new is:merged")).isEmpty();
+    assertThat(status("(is:new is:draft) (is:merged)")).isEmpty();
+    assertThat(status("(is:new is:draft) (is:merged)")).isEmpty();
+
+    assertThat(status("(is:new is:draft) OR (is:merged)"))
+        .containsExactly(MERGED);
+  }
+
+  @Test
+  public void testUnsupportedIndexOperator() throws Exception {
+    Predicate<ChangeData> in = parse("status:merged file:a");
+    assertThat(rewrite(in)).isEqualTo(query(in));
+
+    indexes.setSearchIndex(new FakeIndex(FakeIndex.V1));
+    Predicate<ChangeData> out = rewrite(in);
+    assertThat(out).isInstanceOf(AndPredicate.class);
+    assertThat(out.getChildren())
+        .containsExactly(
+          query(in.getChild(0)),
+          in.getChild(1))
+        .inOrder();
+  }
+
+  @Test
+  public void testTooManyTerms() throws Exception {
+    String q = "file:a OR file:b OR file:c";
+    Predicate<ChangeData> in = parse(q);
+    assertEquals(query(in), rewrite(in));
+
+    exception.expect(QueryParseException.class);
+    exception.expectMessage("too many terms in query");
+    rewrite(parse(q + " OR file:d"));
+  }
+
+  @Test
+  public void testConvertOptions() throws Exception {
+    assertEquals(options(0, 3), convertOptions(options(0, 3)));
+    assertEquals(options(0, 4), convertOptions(options(1, 3)));
+    assertEquals(options(0, 5), convertOptions(options(2, 3)));
+  }
+
+  @Test
+  public void testAddingStartToLimitDoesNotExceedBackendLimit() throws Exception {
+    int max = CONFIG.maxLimit();
+    assertEquals(options(0, max), convertOptions(options(0, max)));
+    assertEquals(options(0, max), convertOptions(options(1, max)));
+    assertEquals(options(0, max), convertOptions(options(1, max - 1)));
+    assertEquals(options(0, max), convertOptions(options(2, max - 1)));
+  }
+
+  private Predicate<ChangeData> parse(String query) throws QueryParseException {
+    return queryBuilder.parse(query);
+  }
+
+  @SafeVarargs
+  private static AndSource andSource(Predicate<ChangeData>... preds) {
+    return new AndSource(Arrays.asList(preds));
+  }
+
+  private Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
+      throws QueryParseException {
+    return rewrite.rewrite(in, options(0, DEFAULT_MAX_QUERY_LIMIT));
+  }
+
+  private Predicate<ChangeData> rewrite(Predicate<ChangeData> in,
+      QueryOptions opts) throws QueryParseException {
+    return rewrite.rewrite(in, opts);
+  }
+
+  private IndexedChangeQuery query(Predicate<ChangeData> p)
+      throws QueryParseException {
+    return query(p, DEFAULT_MAX_QUERY_LIMIT);
+  }
+
+  private IndexedChangeQuery query(Predicate<ChangeData> p, int limit)
+      throws QueryParseException {
+    return new IndexedChangeQuery(index, p, options(0, limit));
+  }
+
+  private static QueryOptions options(int start, int limit) {
+    return QueryOptions.create(CONFIG, start, limit);
+  }
+
+  private Set<Change.Status> status(String query) throws QueryParseException {
+    return IndexRewriter.getPossibleStatus(parse(query));
+  }
+}
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 2f2f234..feca80b 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
@@ -17,7 +17,6 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.TruthJUnit.assume;
 import static com.google.gerrit.extensions.client.ListChangesOption.REVIEWED;
-
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -1274,13 +1273,8 @@
     // necessary for notedb anyway.
     cd.isMergeable();
 
-    // Don't use ExpectedException since that wouldn't distinguish between
-    // failures here and on the previous calls.
-    try {
-      cd.messages();
-    } catch (AssertionError e) {
-      assertThat(e.getMessage()).isEqualTo(DisabledReviewDb.MESSAGE);
-    }
+    exception.expect(DisabledReviewDb.Disabled.class);
+    cd.messages();
   }
 
 
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 d5444e3..9acae6d 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
@@ -30,7 +30,6 @@
 import com.google.gerrit.reviewdb.server.ChangeMessageAccess;
 import com.google.gerrit.reviewdb.server.PatchLineCommentAccess;
 import com.google.gerrit.reviewdb.server.PatchSetAccess;
-import com.google.gerrit.reviewdb.server.PatchSetAncestorAccess;
 import com.google.gerrit.reviewdb.server.PatchSetApprovalAccess;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.SchemaVersionAccess;
@@ -42,7 +41,13 @@
 
 /** ReviewDb that is disabled for testing. */
 public class DisabledReviewDb implements ReviewDb {
-  public static final String MESSAGE = "ReviewDb is disabled for this test";
+  public static class Disabled extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    private Disabled() {
+      super("ReviewDb is disabled for this test");
+    }
+  }
 
   @Override
   public void close() {
@@ -51,156 +56,151 @@
 
   @Override
   public void commit() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public void rollback() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public void updateSchema(StatementExecutor e) {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public void pruneSchema(StatementExecutor e) {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public Access<?, ?>[] allRelations() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public SchemaVersionAccess schemaVersion() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public SystemConfigAccess systemConfig() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public AccountAccess accounts() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public AccountExternalIdAccess accountExternalIds() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public AccountSshKeyAccess accountSshKeys() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public AccountGroupAccess accountGroups() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public AccountGroupNameAccess accountGroupNames() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public AccountGroupMemberAccess accountGroupMembers() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public AccountGroupMemberAuditAccess accountGroupMembersAudit() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public AccountDiffPreferenceAccess accountDiffPreferences() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public StarredChangeAccess starredChanges() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public AccountProjectWatchAccess accountProjectWatches() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public AccountPatchReviewAccess accountPatchReviews() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public ChangeAccess changes() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public PatchSetApprovalAccess patchSetApprovals() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public ChangeMessageAccess changeMessages() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public PatchSetAccess patchSets() {
-    throw new AssertionError(MESSAGE);
-  }
-
-  @Override
-  public PatchSetAncestorAccess patchSetAncestors() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public PatchLineCommentAccess patchComments() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public SubmoduleSubscriptionAccess submoduleSubscriptions() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public AccountGroupByIdAccess accountGroupById() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public AccountGroupByIdAudAccess accountGroupByIdAud() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public int nextAccountId() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public int nextAccountGroupId() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public int nextChangeId() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 
   @Override
   public int nextChangeMessageId() {
-    throw new AssertionError(MESSAGE);
+    throw new Disabled();
   }
 }
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 577eb55..3afb208 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
@@ -712,7 +712,8 @@
           @Override
           public FileSystemView getNormalizedView() {
             return this;
-          }};
+          }
+        };
       }
     });
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index d20a879..acbc50e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -75,7 +75,8 @@
       @Override
       public String apply(AccountGroup.Id id) {
         return id.toString();
-      }});
+      }
+    });
     try {
       createAccountFactory.create(username).apply(TopLevelResource.INSTANCE, input);
     } catch (RestApiException e) {
diff --git a/plugins/cookbook-plugin b/plugins/cookbook-plugin
index 226f4d4..82eefc2 160000
--- a/plugins/cookbook-plugin
+++ b/plugins/cookbook-plugin
@@ -1 +1 @@
-Subproject commit 226f4d41673257bc5b6f95deae49a49aaabde750
+Subproject commit 82eefc2048a4dd69ab589213190dd8403295fb7d