Merge "Revert "Fix GWT UI AddFileBox to provide path suggestions continuously""
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 6ae9995..434dc95 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -20,3 +20,5 @@
 
 # Format all Java files with google-java-format
 292fa154c18b5a203c1aa211dce4efd54e6e8e0a
+111168482164db0cbe6a31630908ab607abc9989
+42ace3c4f41363b41486fe6cf8a3519f296ba4f4
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
index 5d6a854..48a166c 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/ConfigAnnotationParser.java
@@ -59,7 +59,7 @@
       }
     } else {
       throw new IllegalArgumentException(
-          "GerritConfig.name must be of the format" + " section.subsection.name or section.name");
+          "GerritConfig.name must be of the format section.subsection.name or section.name");
     }
   }
 }
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 df82e21..13baea9f 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
@@ -1214,7 +1214,7 @@
     assertThat(msg.rcpt()).containsExactly(user.emailAddress);
     assertThat(msg.body()).contains(admin.fullName + " has removed a vote on this change.\n");
     assertThat(msg.body())
-        .contains("Removed Code-Review+1 by " + user.fullName + " <" + user.email + ">" + "\n");
+        .contains("Removed Code-Review+1 by " + user.fullName + " <" + user.email + ">\n");
 
     Map<String, Short> m =
         gApi.changes().id(r.getChangeId()).reviewer(user.getId().toString()).votes();
@@ -2140,7 +2140,7 @@
             testRepo,
             "Ignore Verified",
             "rules.pl",
-            "submit_rule(submit(CR)) :-\n" + "  gerrit:max_with_block(-2, 2, 'Code-Review', CR).");
+            "submit_rule(submit(CR)) :-\n  gerrit:max_with_block(-2, 2, 'Code-Review', CR).");
     push2.to(RefNames.REFS_CONFIG);
 
     change = gApi.changes().id(r.getChangeId()).get();
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 50cef79..cde75bc 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
@@ -367,7 +367,7 @@
     PushOneCommit.Result r1 = createChange();
 
     // Push another new change (change 2)
-    String subject = "Test change\n\n" + "Change-Id: Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
+    String subject = "Test change\n\nChange-Id: Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
     PushOneCommit push =
         pushFactory.create(
             db, admin.getIdent(), testRepo, subject, "another_file.txt", "another content");
@@ -559,7 +559,7 @@
 
     exception.expect(BadRequestException.class);
     exception.expectMessage(
-        "Cherry Pick: Parent 0 does not exist. Please" + " specify a parent in range [1, 2].");
+        "Cherry Pick: Parent 0 does not exist. Please specify a parent in range [1, 2].");
     gApi.changes().id(mergeChangeResult.getChangeId()).current().cherryPick(cherryPickInput);
   }
 
@@ -580,7 +580,7 @@
 
     exception.expect(BadRequestException.class);
     exception.expectMessage(
-        "Cherry Pick: Parent 3 does not exist. Please" + " specify a parent in range [1, 2].");
+        "Cherry Pick: Parent 3 does not exist. Please specify a parent in range [1, 2].");
     gApi.changes().id(mergeChangeResult.getChangeId()).current().cherryPick(cherryPickInput);
   }
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
index faa21cf..5241f52 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/api/revision/RobotCommentsIT.java
@@ -197,7 +197,7 @@
     exception.expect(BadRequestException.class);
     exception.expectMessage(
         String.format(
-            "A description is required for the " + "suggested fix of the robot comment on %s",
+            "A description is required for the suggested fix of the robot comment on %s",
             withFixRobotCommentInput.path));
     addRobotComment(changeId, withFixRobotCommentInput);
   }
@@ -256,7 +256,7 @@
     exception.expect(BadRequestException.class);
     exception.expectMessage(
         String.format(
-            "A file path must be given for the " + "replacement of the robot comment on %s",
+            "A file path must be given for the replacement of the robot comment on %s",
             withFixRobotCommentInput.path));
     addRobotComment(changeId, withFixRobotCommentInput);
   }
@@ -301,7 +301,7 @@
     exception.expect(BadRequestException.class);
     exception.expectMessage(
         String.format(
-            "A range must be given for the " + "replacement of the robot comment on %s",
+            "A range must be given for the replacement of the robot comment on %s",
             withFixRobotCommentInput.path));
     addRobotComment(changeId, withFixRobotCommentInput);
   }
@@ -315,7 +315,7 @@
     exception.expect(BadRequestException.class);
     exception.expectMessage(
         String.format(
-            "Range (13:9 - 5:10) is not " + "valid for the replacement of the robot comment on %s",
+            "Range (13:9 - 5:10) is not valid for the replacement of the robot comment on %s",
             withFixRobotCommentInput.path));
     addRobotComment(changeId, withFixRobotCommentInput);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
index 72a391c..03c182dd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/AbstractPushForReview.java
@@ -161,7 +161,7 @@
     r1.assertOkStatus();
     r1.assertChange(Change.Status.NEW, null);
     r1.assertMessage(
-        "New changes:\n" + "  " + url + id1 + " " + r1.getCommit().getShortMessage() + "\n");
+        "New changes:\n  " + url + id1 + " " + r1.getCommit().getShortMessage() + "\n");
 
     testRepo.reset(initialHead);
     String newMsg = r1.getCommit().getShortMessage() + " v2";
@@ -238,7 +238,7 @@
     gApi.accounts().self().setWatchedProjects(projectsToWatch);
 
     TestAccount user2 = accounts.user2();
-    String pushSpec = "refs/for/master" + "%reviewer=" + user.email + ",cc=" + user2.email;
+    String pushSpec = "refs/for/master%reviewer=" + user.email + ",cc=" + user2.email;
 
     sender.clear();
     PushOneCommit.Result r = pushTo(pushSpec + ",notify=" + NotifyHandling.NONE);
@@ -991,7 +991,7 @@
   }
 
   private void testpushWithInvalidChangeId() throws Exception {
-    createCommit(testRepo, "Message with invalid Change-Id\n" + "\n" + "Change-Id: X\n");
+    createCommit(testRepo, "Message with invalid Change-Id\n\nChange-Id: X\n");
     pushForReviewRejected(testRepo, "invalid Change-Id line format in commit message footer");
 
     ProjectConfig config = projectCache.checkedGet(project).getConfig();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
index 068eb4f..0c81a35 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/SubmoduleSubscriptionsWholeTopicMergeIT.java
@@ -320,7 +320,7 @@
         .getAdvertisedRef("refs/heads/master")
         .getObjectId();
 
-    assertWithMessage("submodule subscription update " + "should have made one commit")
+    assertWithMessage("submodule subscription update should have made one commit")
         .that(superRepo.getRepository().resolve("origin/master^"))
         .isEqualTo(superPreviousId);
   }
@@ -403,7 +403,7 @@
         .getAdvertisedRef("refs/heads/master")
         .getObjectId();
 
-    assertWithMessage("submodule subscription update " + "should have made one commit")
+    assertWithMessage("submodule subscription update should have made one commit")
         .that(superRepo.getRepository().resolve("origin/master^"))
         .isEqualTo(superPreviousId);
   }
@@ -450,7 +450,7 @@
         .getAdvertisedRef("refs/heads/master")
         .getObjectId();
 
-    assertWithMessage("submodule subscription update " + "should have made one commit")
+    assertWithMessage("submodule subscription update should have made one commit")
         .that(superRepo.getRepository().resolve("origin/master^"))
         .isEqualTo(superPreviousId);
   }
@@ -492,7 +492,7 @@
         .getAdvertisedRef("refs/heads/master")
         .getObjectId();
 
-    assertWithMessage("submodule subscription update " + "should have made one commit")
+    assertWithMessage("submodule subscription update should have made one commit")
         .that(superRepo.getRepository().resolve("origin/master^"))
         .isEqualTo(superPreviousId);
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
index 12b3435..87436e7 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ActionsIT.java
@@ -105,8 +105,7 @@
       assertThat(info.enabled).isNull();
       assertThat(info.label).isEqualTo("Submit whole topic");
       assertThat(info.method).isEqualTo("POST");
-      assertThat(info.title)
-          .isEqualTo("This change depends on other " + "changes which are not ready");
+      assertThat(info.title).isEqualTo("This change depends on other changes which are not ready");
     } else {
       noSubmitWholeTopicAssertions(actions, 1);
 
@@ -307,7 +306,7 @@
       assertThat(info.title)
           .isEqualTo(
               String.format(
-                  "Submit patch set 1 and ancestors (%d changes " + "altogether) into master",
+                  "Submit patch set 1 and ancestors (%d changes altogether) into master",
                   nrChanges));
     }
   }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
index 5dc8a0c..66966c3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ChangeReviewersIT.java
@@ -85,8 +85,7 @@
     assertThat(result.input).isEqualTo(mediumGroup);
     assertThat(result.confirm).isTrue();
     assertThat(result.error)
-        .contains(
-            "has " + mediumGroupSize + " members. Do you want to add them" + " all as reviewers?");
+        .contains("has " + mediumGroupSize + " members. Do you want to add them all as reviewers?");
     assertThat(result.reviewers).isNull();
 
     // Add medium group with confirmation.
@@ -514,8 +513,7 @@
     assertThat(reviewerResult).isNotNull();
     assertThat(reviewerResult.confirm).isTrue();
     assertThat(reviewerResult.error)
-        .contains(
-            "has " + mediumGroupSize + " members. Do you want to add them all" + " as reviewers?");
+        .contains("has " + mediumGroupSize + " members. Do you want to add them all as reviewers?");
 
     // No labels should have changed, and no reviewers/CCs should have been added.
     c = gApi.changes().id(r.getChangeId()).get();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
index cef5e6e..c1784c2 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/ConfigChangeIT.java
@@ -155,7 +155,7 @@
             .asString();
 
     // Append and push malformed project config
-    String pattern = "[access]\n" + "\tinheritFrom = " + allProjects.get() + "\n";
+    String pattern = "[access]\n\tinheritFrom = " + allProjects.get() + "\n";
     String doubleInherit = pattern + "\tinheritFrom = " + parent.get() + "\n";
     config = config.replace(pattern, doubleInherit);
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
index d524f2a..ff4eb3d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
@@ -72,7 +72,7 @@
     assertThat(msg.rcpt()).containsExactly(user.emailAddress);
     assertThat(msg.body()).contains(admin.fullName + " has removed a vote on this change.\n");
     assertThat(msg.body())
-        .contains("Removed Code-Review+1 by " + user.fullName + " <" + user.email + ">" + "\n");
+        .contains("Removed Code-Review+1 by " + user.fullName + " <" + user.email + ">\n");
 
     endPoint =
         "/changes/"
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
index 295aefd..75e7356 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/SubmitByMergeIfNecessaryIT.java
@@ -393,7 +393,7 @@
         createChange(
             repo3,
             "master",
-            "some accompanying changes for change3a in another repo " + "tied together via topic",
+            "some accompanying changes for change3a in another repo tied together via topic",
             "a.txt",
             "1",
             "a-topic-here");
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushTagIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushTagIT.java
index 4bd8df4..7ed15f4 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushTagIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/PushTagIT.java
@@ -204,7 +204,7 @@
     commit(user.getIdent(), "subject");
 
     boolean createTag = tagName == null;
-    tagName = MoreObjects.firstNonNull(tagName, "v1" + "_" + System.nanoTime());
+    tagName = MoreObjects.firstNonNull(tagName, "v1_" + System.nanoTime());
     switch (tagType) {
       case LIGHTWEIGHT:
         break;
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index b76a96a..a427dd3 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -154,7 +154,7 @@
     assertProblems(
         ctl,
         null,
-        problem("Invalid revision on patch set 1:" + " fooooooooooooooooooooooooooooooooooooooo"));
+        problem("Invalid revision on patch set 1: fooooooooooooooooooooooooooooooooooooooo"));
   }
 
   // No test for ref existing but object missing; InMemoryRepository won't let
@@ -170,7 +170,7 @@
         ctl,
         null,
         problem("Ref missing: " + ps.getId().toRefName()),
-        problem("Object missing: patch set 2:" + " deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+        problem("Object missing: patch set 2: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
   }
 
   @Test
@@ -725,7 +725,7 @@
         testRepo
             .commit()
             .parent(parent)
-            .message(commit.getShortMessage() + "\n" + "\n" + "Change-Id: " + badId + "\n")
+            .message(commit.getShortMessage() + "\n\nChange-Id: " + badId + "\n")
             .create();
     testRepo.getRevWalk().parseBody(mergedAs);
     assertThat(mergedAs.getFooterLines(FooterConstants.CHANGE_ID)).containsExactly(badId);
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
index ef5e7cf..da6aef6 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/NoteDbPrimaryIT.java
@@ -17,6 +17,7 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assert_;
 import static com.google.common.truth.TruthJUnit.assume;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.formatTime;
 import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
 import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
@@ -27,6 +28,8 @@
 import com.github.rholder.retry.RetryerBuilder;
 import com.github.rholder.retry.StopStrategies;
 import com.google.common.base.Throwables;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -34,6 +37,7 @@
 import com.google.gerrit.common.TimeUtil;
 import com.google.gerrit.extensions.api.changes.DraftInput;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
 import com.google.gerrit.extensions.client.ChangeStatus;
 import com.google.gerrit.extensions.common.ApprovalInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
@@ -41,14 +45,24 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.server.ChangeMessagesUtil;
+import com.google.gerrit.server.CommentsUtil;
+import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.config.AllUsersName;
+import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.RepoRefCache;
+import com.google.gerrit.server.notedb.ChangeBundle;
+import com.google.gerrit.server.notedb.ChangeBundleReader;
 import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.notedb.ChangeUpdate;
 import com.google.gerrit.server.notedb.NoteDbChangeState;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.notedb.PrimaryStorageMigrator;
 import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.NoteDbMode;
 import com.google.gerrit.testutil.TestTimeUtil;
@@ -62,7 +76,10 @@
 import java.util.List;
 import java.util.Optional;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -78,8 +95,13 @@
   }
 
   @Inject private AllUsersName allUsers;
-
+  @Inject private BatchUpdate.Factory batchUpdateFactory;
+  @Inject private ChangeBundleReader bundleReader;
+  @Inject private CommentsUtil commentsUtil;
   @Inject private TestChangeRebuilderWrapper rebuilderWrapper;
+  @Inject private ChangeControl.GenericFactory changeControlFactory;
+  @Inject private ChangeUpdate.Factory updateFactory;
+  @Inject private InternalUser.Factory internalUserFactory;
 
   private PrimaryStorageMigrator migrator;
 
@@ -94,7 +116,17 @@
   private PrimaryStorageMigrator newMigrator(
       @Nullable Retryer<NoteDbChangeState> ensureRebuiltRetryer) {
     return new PrimaryStorageMigrator(
-        cfg, Providers.of(db), repoManager, allUsers, rebuilderWrapper, ensureRebuiltRetryer);
+        cfg,
+        Providers.of(db),
+        repoManager,
+        allUsers,
+        rebuilderWrapper,
+        ensureRebuiltRetryer,
+        changeControlFactory,
+        queryProvider,
+        updateFactory,
+        internalUserFactory,
+        batchUpdateFactory);
   }
 
   @After
@@ -368,6 +400,105 @@
     assertNoteDbPrimary(id);
   }
 
+  @Test
+  public void rebuildReviewDb() throws Exception {
+    Change c = createChange().getChange().change();
+    Change.Id id = c.getId();
+
+    CommentInput cin = new CommentInput();
+    cin.line = 1;
+    cin.message = "Published comment";
+    ReviewInput rin = ReviewInput.approve();
+    rin.comments = ImmutableMap.of(PushOneCommit.FILE_NAME, ImmutableList.of(cin));
+    gApi.changes().id(id.get()).current().review(ReviewInput.approve());
+
+    DraftInput din = new DraftInput();
+    din.path = PushOneCommit.FILE_NAME;
+    din.line = 1;
+    din.message = "Draft comment";
+    gApi.changes().id(id.get()).current().createDraft(din);
+    gApi.changes().id(id.get()).current().review(ReviewInput.approve());
+    gApi.changes().id(id.get()).current().createDraft(din);
+
+    assertThat(db.changeMessages().byChange(id)).isNotEmpty();
+    assertThat(db.patchSets().byChange(id)).isNotEmpty();
+    assertThat(db.patchSetApprovals().byChange(id)).isNotEmpty();
+    assertThat(db.patchComments().byChange(id)).isNotEmpty();
+
+    ChangeBundle noteDbBundle =
+        ChangeBundle.fromNotes(commentsUtil, notesFactory.create(db, project, id));
+
+    setNoteDbPrimary(id);
+
+    db.changeMessages().delete(db.changeMessages().byChange(id));
+    db.patchSets().delete(db.patchSets().byChange(id));
+    db.patchSetApprovals().delete(db.patchSetApprovals().byChange(id));
+    db.patchComments().delete(db.patchComments().byChange(id));
+    ChangeMessage bogusMessage =
+        ChangeMessagesUtil.newMessage(
+            c.currentPatchSetId(),
+            identifiedUserFactory.create(admin.getId()),
+            TimeUtil.nowTs(),
+            "some message",
+            null);
+    db.changeMessages().insert(Collections.singleton(bogusMessage));
+
+    rebuilderWrapper.rebuildReviewDb(db, project, id);
+
+    assertThat(db.changeMessages().byChange(id)).isNotEmpty();
+    assertThat(db.patchSets().byChange(id)).isNotEmpty();
+    assertThat(db.patchSetApprovals().byChange(id)).isNotEmpty();
+    assertThat(db.patchComments().byChange(id)).isNotEmpty();
+
+    ChangeBundle reviewDbBundle = bundleReader.fromReviewDb(ReviewDbUtil.unwrapDb(db), id);
+    assertThat(reviewDbBundle.differencesFrom(noteDbBundle)).isEmpty();
+  }
+
+  @Test
+  public void rebuildReviewDbRequiresNoteDbPrimary() throws Exception {
+    Change.Id id = createChange().getChange().getId();
+
+    exception.expect(OrmException.class);
+    exception.expectMessage("primary storage of " + id + " is REVIEW_DB");
+    rebuilderWrapper.rebuildReviewDb(db, project, id);
+  }
+
+  @Test
+  public void migrateBackToReviewDbPrimary() throws Exception {
+    Change c = createChange().getChange().change();
+    Change.Id id = c.getId();
+
+    migrator.migrateToNoteDbPrimary(id);
+    assertNoteDbPrimary(id);
+
+    gApi.changes().id(id.get()).topic("new-topic");
+    assertThat(gApi.changes().id(id.get()).topic()).isEqualTo("new-topic");
+    assertThat(db.changes().get(id).getTopic()).isNotEqualTo("new-topic");
+
+    migrator.migrateToReviewDbPrimary(id, null);
+    ObjectId metaId;
+    try (Repository repo = repoManager.openRepository(c.getProject());
+        RevWalk rw = new RevWalk(repo)) {
+      metaId = repo.exactRef(RefNames.changeMetaRef(id)).getObjectId();
+      RevCommit commit = rw.parseCommit(metaId);
+      rw.parseBody(commit);
+      assertThat(commit.getFullMessage())
+          .contains("Read-only-until: " + formatTime(serverIdent.get(), new Timestamp(0)));
+    }
+    NoteDbChangeState state = NoteDbChangeState.parse(db.changes().get(id));
+    assertThat(state.getPrimaryStorage()).isEqualTo(PrimaryStorage.REVIEW_DB);
+    assertThat(state.getChangeMetaId()).isEqualTo(metaId);
+    assertThat(gApi.changes().id(id.get()).topic()).isEqualTo("new-topic");
+    assertThat(db.changes().get(id).getTopic()).isEqualTo("new-topic");
+
+    ChangeNotes notes = notesFactory.create(db, project, id);
+    assertThat(notes.getRevision()).isEqualTo(metaId); // No rebuilding, change was up to date.
+    assertThat(notes.getReadOnlyUntil()).isNotNull();
+
+    gApi.changes().id(id.get()).topic("reviewdb-topic");
+    assertThat(db.changes().get(id).getTopic()).isEqualTo("reviewdb-topic");
+  }
+
   private void setNoteDbPrimary(Change.Id id) throws Exception {
     Change c = db.changes().get(id);
     assertThat(c).named("change " + id).isNotNull();
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index e489257..b0c7764 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -572,7 +572,7 @@
         try (Statement s = c.conn.createStatement()) {
           long used = 0;
           try (ResultSet r =
-              s.executeQuery("SELECT" + " SUM(OCTET_LENGTH(k) + OCTET_LENGTH(v))" + " FROM data")) {
+              s.executeQuery("SELECT SUM(OCTET_LENGTH(k) + OCTET_LENGTH(v)) FROM data")) {
             used = r.next() ? r.getLong(1) : 0;
           }
           if (used <= maxSize) {
diff --git a/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java b/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java
index 87d5db3..15e0de0 100644
--- a/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java
+++ b/gerrit-cache-h2/src/test/java/com/google/gerrit/server/cache/h2/H2CacheTest.java
@@ -42,7 +42,7 @@
 
     TypeLiteral<String> keyType = new TypeLiteral<String>() {};
     SqlStore<String, Boolean> store =
-        new SqlStore<>("jdbc:h2:mem:" + "Test_" + (++dbCnt), keyType, 1 << 20, 0);
+        new SqlStore<>("jdbc:h2:mem:Test_" + (++dbCnt), keyType, 1 << 20, 0);
     impl = new H2CacheImpl<>(MoreExecutors.directExecutor(), store, keyType, mem);
   }
 
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 fe987f4..b74139a 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
@@ -237,7 +237,7 @@
   private static String missingUserIds(Set<String> allowedUserIds) {
     StringBuilder sb =
         new StringBuilder(
-            "Key must contain a valid" + " certification for one of the following identities:\n");
+            "Key must contain a valid certification for one of the following identities:\n");
     Iterator<String> sorted = allowedUserIds.stream().sorted().iterator();
     while (sorted.hasNext()) {
       sb.append("  ").append(sorted.next());
diff --git a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GpgModule.java b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GpgModule.java
index 6d93184..d12e921 100644
--- a/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GpgModule.java
+++ b/gerrit-gpg/src/main/java/com/google/gerrit/gpg/GpgModule.java
@@ -39,7 +39,7 @@
     bindConstant().annotatedWith(EnableSignedPush.class).to(enableSignedPush);
 
     if (configEnableSignedPush && !havePgp) {
-      log.info("Bouncy Castle PGP not installed; signed push verification is" + " disabled");
+      log.info("Bouncy Castle PGP not installed; signed push verification is disabled");
     }
     if (enableSignedPush) {
       install(new SignedPushModule());
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 011f54b..420dd50 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
@@ -229,7 +229,7 @@
     assertProblems(
         checker.check(key.getPublicKey()),
         Status.BAD,
-        "No identities found for user; check" + " http://test/#/settings/web-identities");
+        "No identities found for user; check http://test/#/settings/web-identities");
 
     checker = checkerFactory.create().setStore(store).disableTrust();
     assertProblems(
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 5da23a5..39e2cb4 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
@@ -197,7 +197,7 @@
     add(validKeyWithoutExpiration());
     save();
 
-    assertProblems(k, "Key is revoked (key material has been compromised):" + " test6 compromised");
+    assertProblems(k, "Key is revoked (key material has been compromised): test6 compromised");
 
     PGPPublicKeyRing kr = removeRevokers(k.getPublicKeyRing());
     store.add(kr);
@@ -227,7 +227,7 @@
     TestKey k = add(revokedCompromisedKey());
     save();
 
-    assertProblems(k, "Key is revoked (key material has been compromised):" + " test6 compromised");
+    assertProblems(k, "Key is revoked (key material has been compromised): test6 compromised");
   }
 
   @Test
diff --git a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
index 51a5a0c..9161652 100644
--- a/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
+++ b/gerrit-gwtexpui/src/main/java/com/google/gwtexpui/safehtml/client/SafeHtml.java
@@ -126,10 +126,9 @@
 
   /** Convert bare http:// and https:// URLs into &lt;a href&gt; tags. */
   public SafeHtml linkify() {
-    final String part =
-        "(?:" + "[a-zA-Z0-9$_+!*'%;:@=?#/~-]" + "|&(?!lt;|gt;)" + "|[.,](?!(?:\\s|$))" + ")";
+    final String part = "(?:[a-zA-Z0-9$_+!*'%;:@=?#/~-]|&(?!lt;|gt;)|[.,](?!(?:\\s|$)))";
     return replaceAll(
-        "(https?://" + part + "{2,}" + "(?:[(]" + part + "*" + "[)])*" + part + "*" + ")",
+        "(https?://" + part + "{2,}(?:[(]" + part + "*[)])*" + part + "*)",
         "<a href=\"$1\" target=\"_blank\" rel=\"nofollow\">$1</a>");
   }
 
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
index 19e707a..ac0f6fd6 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_ReplaceTest.java
@@ -54,8 +54,7 @@
         o.replaceAll(repls(new RawFindReplace("(issue\\s(\\d+))", "<a href=\"?$2\">$1</a>")));
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString())
-        .isEqualTo(
-            "A\n" + "<a href=\"?42\">issue 42</a>\n" + "<a href=\"?9918\">issue 9918</a>\n" + "B");
+        .isEqualTo("A\n<a href=\"?42\">issue 42</a>\n<a href=\"?9918\">issue 9918</a>\nB");
   }
 
   @Test
diff --git a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
index 2544c68..1346cda 100644
--- a/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
+++ b/gerrit-gwtexpui/src/test/java/com/google/gwtexpui/safehtml/client/SafeHtml_WikifyPreformatTest.java
@@ -32,7 +32,7 @@
     final SafeHtml n = o.wikify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString())
-        .isEqualTo("<p>A</p>" + "<p>" + pre("  This is pre") + pre("  formatted") + "</p>");
+        .isEqualTo("<p>A</p><p>" + pre("  This is pre") + pre("  formatted") + "</p>");
   }
 
   @Test
@@ -72,7 +72,7 @@
     final SafeHtml n = o.wikify();
     assertThat(o).isNotSameAs(n);
     assertThat(n.asString())
-        .isEqualTo("<p>" + pre("  Q") + pre("    &lt;R&gt;") + pre("  S") + "</p>" + "<p>B</p>");
+        .isEqualTo("<p>" + pre("  Q") + pre("    &lt;R&gt;") + pre("  S") + "</p><p>B</p>");
   }
 
   private static SafeHtml html(String text) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
index 60482d3..88f9b4c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -181,8 +181,7 @@
 
   private boolean failAuthentication(Response rsp, String username) throws IOException {
     log.warn(
-        "Authentication failed for {}: password does not match the one" + " stored in Gerrit",
-        username);
+        "Authentication failed for {}: password does not match the one stored in Gerrit", username);
     rsp.sendError(SC_UNAUTHORIZED);
     return false;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
index 76ea665..28d12ee 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -71,7 +71,7 @@
     if (sessionMaxAgeMillis < MINUTES.toMillis(5)) {
       log.warn(
           String.format(
-              "cache.%s.maxAge is set to %d milliseconds;" + " it should be at least 5 minutes.",
+              "cache.%s.maxAge is set to %d milliseconds; it should be at least 5 minutes.",
               CACHE_NAME, sessionMaxAgeMillis));
     }
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/PrologShell.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/PrologShell.java
index 83bcf4a..5decd68 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/PrologShell.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/PrologShell.java
@@ -69,7 +69,7 @@
         com.google.gerrit.common.Version.getVersion());
     System.err.println();
     System.err.println(
-        "(type Ctrl-D or \"halt.\" to exit," + " \"['path/to/file.pl'].\" to load a file)");
+        "(type Ctrl-D or \"halt.\" to exit, \"['path/to/file.pl'].\" to load a file)");
     System.err.println();
     System.err.flush();
   }
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 43a44cd..ae5a598 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
@@ -208,7 +208,7 @@
       }
       return names;
     } catch (FileNotFoundException e) {
-      log.warn("Couldn't find distribution archive location." + " No plugin will be installed");
+      log.warn("Couldn't find distribution archive location. No plugin will be installed");
       return null;
     }
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
index af4c7d8..f434681 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
@@ -323,7 +323,7 @@
 
     } else if (!ui.yesno(
         null /* force an answer */,
-        "error: SHA-1 checksum does not match\n" + "Use %s anyway", //
+        "error: SHA-1 checksum does not match\nUse %s anyway", //
         dst.getFileName())) {
       deleteDst();
       throw new Die("aborted by user");
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 cc9ddf6..279584a 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
@@ -96,8 +96,7 @@
   /** Ensures we are running inside of a valid site, otherwise throws a Die. */
   protected void mustHaveValidSite() throws Die {
     if (!Files.exists(sitePath.resolve("etc").resolve("gerrit.config"))) {
-      throw die(
-          "not a Gerrit site: '" + getSitePath() + "'\n" + "Perhaps you need to run init first?");
+      throw die("not a Gerrit site: '" + getSitePath() + "'\nPerhaps you need to run init first?");
     }
   }
 
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 026e32a..2c4c61c 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
@@ -364,7 +364,7 @@
     if (user == null) {
       user = "";
     }
-    user = user + "|" + "account-" + ua.getId().toString();
+    user = user + "|account-" + ua.getId().toString();
 
     return new PersonIdent(name, user + "@" + guessHost(), when, tz);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
index 8c46663..5ca9e19 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StarredChangesUtil.java
@@ -134,7 +134,7 @@
     static IllegalLabelException mutuallyExclusiveLabels(String label1, String label2) {
       return new IllegalLabelException(
           String.format(
-              "The labels %s and %s are mutually exclusive." + " Only one of them can be set.",
+              "The labels %s and %s are mutually exclusive. Only one of them can be set.",
               label1, label2));
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
index cf253ff..45f9183 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CreateAccount.java
@@ -113,7 +113,7 @@
 
     if (!username.matches(Account.USER_NAME_PATTERN)) {
       throw new BadRequestException(
-          "Username '" + username + "'" + " must contain only letters, numbers, _, - or .");
+          "Username '" + username + "' must contain only letters, numbers, _, - or .");
     }
 
     Set<AccountGroup.Id> groups = parseGroups(input.groups);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
index b3a99eb..97102a2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DeleteWatchedProjects.java
@@ -53,7 +53,7 @@
       throws AuthException, UnprocessableEntityException, OrmException, IOException,
           ConfigInvalidException {
     if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) {
-      throw new AuthException("It is not allowed to edit project watches " + "of other users");
+      throw new AuthException("It is not allowed to edit project watches of other users");
     }
     if (input == null) {
       return Response.none();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java
index f61704a..e0aeee0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetWatchedProjects.java
@@ -52,7 +52,7 @@
   public List<ProjectWatchInfo> apply(AccountResource rsrc)
       throws OrmException, AuthException, IOException, ConfigInvalidException {
     if (self.get() != rsrc.getUser() && !self.get().getCapabilities().canAdministrateServer()) {
-      throw new AuthException("It is not allowed to list project watches " + "of other users");
+      throw new AuthException("It is not allowed to list project watches of other users");
     }
     Account.Id accountId = rsrc.getUser().getAccountId();
     List<ProjectWatchInfo> projectWatchInfos = new ArrayList<>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
index fe2a94b..bdf0c91 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
@@ -50,7 +50,7 @@
     final String[] tokens = token.split(",");
     if (tokens.length != 3) {
       throw new CmdLineException(
-          owner, "change should be specified as " + "<project>,<branch>,<change-id>");
+          owner, "change should be specified as <project>,<branch>,<change-id>");
     }
 
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java
index b0360e4..3ad97b0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java
@@ -57,7 +57,7 @@
 
     String msg =
         String.format(
-            "Multiple AuthBackends attempted to handle request:" + " authUsers=%s authExs=%s",
+            "Multiple AuthBackends attempted to handle request: authUsers=%s authExs=%s",
             authUsers, authExs);
     throw new AuthException(msg);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
index 2225460..fbb2115 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeEdits.java
@@ -377,7 +377,7 @@
     @Option(
       name = "--base",
       aliases = {"-b"},
-      usage = "whether to load the content on the base revision instead of the" + " change edit"
+      usage = "whether to load the content on the base revision instead of the change edit"
     )
     private boolean base;
 
@@ -478,7 +478,7 @@
     @Option(
       name = "--base",
       aliases = {"-b"},
-      usage = "whether to load the message on the base revision instead" + " of the change edit"
+      usage = "whether to load the message on the base revision instead of the change edit"
     )
     private boolean base;
 
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 1edf9b4..6ee71d9 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
@@ -374,7 +374,7 @@
       if (!rw.isMergedInto(commit, tip)) {
         problem(
             String.format(
-                "Expected merged commit %s is not merged into" + " destination ref %s (%s)",
+                "Expected merged commit %s is not merged into destination ref %s (%s)",
                 commit.name(), change().getDest().get(), tip.name()));
         return;
       }
@@ -412,7 +412,7 @@
           if (changeId != null && !changeId.equals(change().getKey().get())) {
             problem(
                 String.format(
-                    "Expected merged commit %s has Change-Id: %s," + " but expected %s",
+                    "Expected merged commit %s has Change-Id: %s, but expected %s",
                     commit.name(), changeId, change().getKey().get()));
             return;
           }
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 d0c0e29..e060f8d 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
@@ -552,7 +552,7 @@
     if (description == null) {
       throw new BadRequestException(
           String.format(
-              "A description is required " + "for the suggested fix of the robot comment on %s",
+              "A description is required for the suggested fix of the robot comment on %s",
               commentPath));
     }
   }
@@ -586,7 +586,7 @@
     if (replacementPath == null) {
       throw new BadRequestException(
           String.format(
-              "A file path must be given " + "for the replacement of the robot comment on %s",
+              "A file path must be given for the replacement of the robot comment on %s",
               commentPath));
     }
   }
@@ -608,8 +608,7 @@
     if (range == null) {
       throw new BadRequestException(
           String.format(
-              "A range must be given " + "for the replacement of the robot comment on %s",
-              commentPath));
+              "A range must be given for the replacement of the robot comment on %s", commentPath));
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
index 8b71eca..67c0a6a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PreviewSubmit.java
@@ -152,7 +152,7 @@
                 }
               } catch (LimitExceededException e) {
                 throw new NotImplementedException(
-                    "The bundle is too big to " + "generate at the server");
+                    "The bundle is too big to generate at the server");
               }
             }
           };
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
index 88fc1b3..173f522 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RebaseUtil.java
@@ -164,7 +164,7 @@
       throw new UnprocessableEntityException("Cannot rebase a change with multiple parents.");
     } else if (commit.getParentCount() == 0) {
       throw new UnprocessableEntityException(
-          "Cannot rebase a change without any parents" + " (is this the initial commit?).");
+          "Cannot rebase a change without any parents (is this the initial commit?).");
     }
 
     RevId parentRev = new RevId(commit.getParent(0).name());
@@ -184,7 +184,7 @@
         if (depChange.getStatus().isOpen()) {
           if (depPatchSet.getId().equals(depChange.currentPatchSetId())) {
             throw new ResourceConflictException(
-                "Change is already based on the latest patch set of the" + " dependent change.");
+                "Change is already based on the latest patch set of the dependent change.");
           }
           baseRev = cd.currentPatchSet().getRevision().get();
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
index 8013d87..64e02a8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -318,7 +318,7 @@
       cs = mergeSuperSet.get().completeChangeSet(db, cd.change(), resource.getControl().getUser());
     } catch (OrmException | IOException e) {
       throw new OrmRuntimeException(
-          "Could not determine complete set of " + "changes to be submitted", e);
+          "Could not determine complete set of changes to be submitted", e);
     }
 
     int topicSize = 0;
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 4a7f7da..3d1fb79 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
@@ -111,7 +111,7 @@
     Optional<ChangeEdit> changeEdit = lookupChangeEdit(changeControl);
     if (changeEdit.isPresent()) {
       throw new InvalidChangeOperationException(
-          String.format("A change edit " + "already exists for change %s", changeControl.getId()));
+          String.format("A change edit already exists for change %s", changeControl.getId()));
     }
 
     PatchSet currentPatchSet = lookupCurrentPatchSet(changeControl);
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 3de1243..80dcb78 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
@@ -150,7 +150,7 @@
     try {
       a.commitMessage = changeDataFactory.create(db, change).commitMessage();
     } catch (Exception e) {
-      log.error("Error while getting full commit message for" + " change " + a.number);
+      log.error("Error while getting full commit message for change " + a.number);
     }
     a.url = getChangeUrl(change);
     a.owner = asAccountAttribute(change.getOwner());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdateReviewDb.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdateReviewDb.java
index 1f476fa..084b2e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdateReviewDb.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdateReviewDb.java
@@ -93,7 +93,7 @@
     @Override
     public Change atomicUpdate(Change.Id key, AtomicUpdate<Change> update) {
       throw new UnsupportedOperationException(
-          "do not call atomicUpdate; updateChange is always called within a" + " transaction");
+          "do not call atomicUpdate; updateChange is always called within a transaction");
     }
   }
 }
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 3da17a4..deea815 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
@@ -268,7 +268,7 @@
     } else if (results.isEmpty()) {
       throw new IllegalStateException(
           String.format(
-              "SubmitRuleEvaluator.evaluate for change %s " + "returned empty list for %s in %s",
+              "SubmitRuleEvaluator.evaluate for change %s returned empty list for %s in %s",
               cd.getId(), patchSet.getId(), cd.change().getProject().get()));
     }
 
@@ -733,7 +733,7 @@
     try {
       return orm.openRepo(project);
     } catch (NoSuchProjectException noProject) {
-      logWarn("Project " + noProject.project() + " no longer exists, " + "abandoning open changes");
+      logWarn("Project " + noProject.project() + " no longer exists, abandoning open changes");
       abandonAllOpenChangeForDeletedProject(noProject.project());
     } catch (IOException e) {
       throw new IntegrationException("Error opening project " + project, e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index a994af0..9101b44 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -236,7 +236,7 @@
         if (!done && workerFuture.isDone()) {
           // The worker may not have called end() explicitly, which is likely a
           // programming error.
-          log.warn("MultiProgressMonitor worker did not call end()" + " before returning");
+          log.warn("MultiProgressMonitor worker did not call end() before returning");
           end();
         }
       }
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 aed59e7..a6c35ed 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
@@ -197,7 +197,7 @@
     UPDATE(
         "You are not allowed to perform this operation.\n"
             + "To push into this reference you need 'Push' rights."),
-    DELETE("You need 'Push' rights with the 'Force Push'\n" + "flag set to delete references."),
+    DELETE("You need 'Push' rights with the 'Force Push'\nflag set to delete references."),
     DELETE_CHANGES("Cannot delete from '" + REFS_CHANGES + "'"),
     CODE_REVIEW(
         "You need 'Push' rights to upload code review requests.\n"
@@ -762,7 +762,7 @@
         String refName = replace.inputCommand.getRefName();
         checkState(
             NEW_PATCHSET.matcher(refName).matches(),
-            "expected a new patch set command as input when creating %s;" + " got %s",
+            "expected a new patch set command as input when creating %s; got %s",
             replace.cmd.getRefName(),
             refName);
         try {
@@ -2845,7 +2845,7 @@
       }
 
       logDebug(
-          "Auto-closing {} changes with existing patch sets and {} with" + " new patch sets",
+          "Auto-closing {} changes with existing patch sets and {} with new patch sets",
           existingPatchSets,
           newPatchSets);
       bu.execute();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index 5ed3f97..cdb2c3b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -530,7 +530,7 @@
       }
     } catch (IOException e) {
       throw new SubmoduleException(
-          "Could not perform a revwalk to " + "create superproject commit message", e);
+          "Could not perform a revwalk to create superproject commit message", e);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
index a84ab31..150965c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/MergeValidators.java
@@ -75,7 +75,7 @@
     private static final String INVALID_CONFIG =
         "Change contains an invalid project configuration.";
     private static final String PARENT_NOT_FOUND =
-        "Change contains an invalid project configuration:\n" + "Parent project does not exist.";
+        "Change contains an invalid project configuration:\nParent project does not exist.";
     private static final String PLUGIN_VALUE_NOT_EDITABLE =
         "Change contains an invalid project configuration:\n"
             + "One of the plugin configuration parameters is not editable.";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
index ff35750..f99f3b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/change/IndexedChangeQuery.java
@@ -122,7 +122,7 @@
     Predicate<ChangeData> pred = getChild(0);
     checkState(
         pred.isMatchable(),
-        "match invoked, but child predicate %s " + "doesn't implement %s",
+        "match invoked, but child predicate %s doesn't implement %s",
         pred,
         Matchable.class.getName());
     return pred.asMatchable().match(cd);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java
index 3bf452f..e8e2250 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/receive/Pop3MailReceiver.java
@@ -65,7 +65,7 @@
     try {
       try {
         if (!pop3.login(mailSettings.username, mailSettings.password)) {
-          log.error("Could not login to POP3 email server." + " Check username and password");
+          log.error("Could not login to POP3 email server. Check username and password");
           return;
         }
         try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index fb9564a0..fbecaef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -207,7 +207,7 @@
   }
 
   public String getChangeMessageThreadId() throws EmailException {
-    return velocify("<gerrit.${change.createdOn.time}.$change.key.get()" + "@$email.gerritHost>");
+    return velocify("<gerrit.${change.createdOn.time}.$change.key.get()@$email.gerritHost>");
   }
 
   /** Format the sender's "cover letter", {@link #getCoverLetter()}. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index 841c995..80e6bb8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -192,7 +192,7 @@
       setMissingHeader(
           hdrs,
           "Content-Type",
-          "multipart/alternative; " + "boundary=\"" + boundary + "\"; " + "charset=UTF-8");
+          "multipart/alternative; boundary=\"" + boundary + "\"; charset=UTF-8");
       encodedBody = buildMultipartBody(boundary, textBody, htmlBody);
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 8df9bd6..472eda1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -29,8 +29,10 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
 import java.io.IOException;
+import java.sql.Timestamp;
 import java.util.Date;
 import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -47,15 +49,17 @@
   protected final Account.Id realAccountId;
   protected final PersonIdent authorIdent;
   protected final Date when;
+  private final long readOnlySkewMs;
 
   @Nullable private final ChangeNotes notes;
   private final Change change;
-  private final PersonIdent serverIdent;
+  protected final PersonIdent serverIdent;
 
   protected PatchSet.Id psId;
   private ObjectId result;
 
   protected AbstractChangeUpdate(
+      Config cfg,
       NotesMigration migration,
       ChangeControl ctl,
       PersonIdent serverIdent,
@@ -73,9 +77,11 @@
     this.realAccountId = realAccountId != null ? realAccountId : accountId;
     this.authorIdent = ident(noteUtil, serverIdent, anonymousCowardName, ctl.getUser(), when);
     this.when = when;
+    this.readOnlySkewMs = NoteDbChangeState.getReadOnlySkew(cfg);
   }
 
   protected AbstractChangeUpdate(
+      Config cfg,
       NotesMigration migration,
       ChangeNoteUtil noteUtil,
       PersonIdent serverIdent,
@@ -99,6 +105,7 @@
     this.realAccountId = realAccountId;
     this.authorIdent = authorIdent;
     this.when = when;
+    this.readOnlySkewMs = NoteDbChangeState.getReadOnlySkew(cfg);
   }
 
   private static void checkUserType(CurrentUser user) {
@@ -133,6 +140,14 @@
     return change.getId();
   }
 
+  /**
+   * @return notes for the state of this change prior to this update. If this update is part of a
+   *     series managed by a {@link NoteDbUpdateManager}, then this reflects the state prior to the
+   *     first update in the series. A null return value can only happen when the change is being
+   *     rebuilt from NoteDb. A change that is in the process of being created will result in a
+   *     non-null return value from this method, but a null return value from {@link
+   *     ChangeNotes#getRevision()}.
+   */
   @Nullable
   public ChangeNotes getNotes() {
     return notes;
@@ -206,6 +221,7 @@
     // to actually store.
 
     checkArgument(rw.getObjectReader().getCreatedFromInserter() == ins);
+    checkNotReadOnly();
     ObjectId z = ObjectId.zeroId();
     CommitBuilder cb = applyImpl(rw, ins, curr);
     if (cb == null) {
@@ -233,6 +249,18 @@
     return result;
   }
 
+  protected void checkNotReadOnly() throws OrmException {
+    ChangeNotes notes = getNotes();
+    if (notes == null) {
+      // Can only happen during ChangeRebuilder, which will never include a read-only lease.
+      return;
+    }
+    Timestamp until = notes.getReadOnlyUntil();
+    if (until != null && NoteDbChangeState.timeForReadOnlyCheck(readOnlySkewMs).before(until)) {
+      throw new OrmException("change " + notes.getChangeId() + " is read-only until " + until);
+    }
+  }
+
   /**
    * Create a commit containing the contents of this update.
    *
@@ -267,7 +295,7 @@
     checkArgument(c.revId != null, "RevId required for comment: %s", c);
     checkArgument(
         c.author.getId().equals(getAccountId()),
-        "The author for the following comment does not match the author of" + " this %s (%s): %s",
+        "The author for the following comment does not match the author of this %s (%s): %s",
         getClass().getSimpleName(),
         getAccountId(),
         c);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 74ffb96..428faef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -42,6 +43,7 @@
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -91,6 +93,7 @@
 
   @AssistedInject
   private ChangeDraftUpdate(
+      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
       @AnonymousCowardName String anonymousCowardName,
       NotesMigration migration,
@@ -102,6 +105,7 @@
       @Assisted PersonIdent authorIdent,
       @Assisted Date when) {
     super(
+        cfg,
         migration,
         noteUtil,
         serverIdent,
@@ -117,6 +121,7 @@
 
   @AssistedInject
   private ChangeDraftUpdate(
+      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
       @AnonymousCowardName String anonymousCowardName,
       NotesMigration migration,
@@ -128,6 +133,7 @@
       @Assisted PersonIdent authorIdent,
       @Assisted Date when) {
     super(
+        cfg,
         migration,
         noteUtil,
         serverIdent,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 8d61c36..c848987 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -73,6 +73,7 @@
   public static final FooterKey FOOTER_PATCH_SET = new FooterKey("Patch-set");
   public static final FooterKey FOOTER_PATCH_SET_DESCRIPTION =
       new FooterKey("Patch-set-description");
+  public static final FooterKey FOOTER_READ_ONLY_UNTIL = new FooterKey("Read-only-until");
   public static final FooterKey FOOTER_REAL_USER = new FooterKey("Real-user");
   public static final FooterKey FOOTER_STATUS = new FooterKey("Status");
   public static final FooterKey FOOTER_SUBJECT = new FooterKey("Subject");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
index b15874d..4993a5d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -63,6 +63,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -166,7 +167,7 @@
         checkNotNull(change, "change %s not found in ReviewDb", changeId);
         checkArgument(
             change.getProject().equals(project),
-            "passed project %s when creating ChangeNotes for %s, but actual" + " project is %s",
+            "passed project %s when creating ChangeNotes for %s, but actual project is %s",
             project,
             changeId,
             change.getProject());
@@ -216,7 +217,7 @@
     private ChangeNotes createFromChangeOnlyWhenNoteDbDisabled(Change change) throws OrmException {
       checkState(
           !args.migration.readChanges(),
-          "do not call" + " createFromChangeWhenNoteDbDisabled when NoteDb is enabled");
+          "do not call createFromChangeWhenNoteDbDisabled when NoteDb is enabled");
       return new ChangeNotes(args, change).load();
     }
 
@@ -326,15 +327,14 @@
         Change change = readOneReviewDbChange(db, id);
         if (change == null) {
           if (defaultStorage == PrimaryStorage.REVIEW_DB) {
-            log.warn(
-                "skipping change {} found in project {} " + "but not in ReviewDb", id, project);
+            log.warn("skipping change {} found in project {} but not in ReviewDb", id, project);
             continue;
           }
           // TODO(dborowitz): See discussion in BatchUpdate#newChangeContext.
           change = newNoteDbOnlyChange(project, id);
         } else if (!change.getProject().equals(project)) {
           log.error(
-              "skipping change {} found in project {} " + "because ReviewDb change has project {}",
+              "skipping change {} found in project {} because ReviewDb change has project {}",
               id,
               project,
               change.getProject());
@@ -558,6 +558,11 @@
     return checkNotNull(getPatchSets().get(psId), "missing current patch set %s", psId.get());
   }
 
+  @VisibleForTesting
+  public Timestamp getReadOnlyUntil() {
+    return state.readOnlyUntil();
+  }
+
   @Override
   protected void onLoad(LoadHandle handle)
       throws NoSuchChangeException, IOException, ConfigInvalidException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 2a9d985..dac999c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -24,6 +24,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET_DESCRIPTION;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_READ_ONLY_UNTIL;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBJECT;
@@ -68,6 +69,7 @@
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.sql.Timestamp;
+import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -76,6 +78,7 @@
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
@@ -89,6 +92,7 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.FooterKey;
+import org.eclipse.jgit.util.GitDateParser;
 import org.eclipse.jgit.util.RawParseUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -152,6 +156,7 @@
   private String submissionId;
   private String tag;
   private RevisionNoteMap<ChangeRevisionNote> revisionNoteMap;
+  private Timestamp readOnlyUntil;
 
   ChangeNotesParser(
       Change.Id changeId,
@@ -232,7 +237,8 @@
         submitRecords,
         buildAllMessages(),
         buildMessagesByPatchSet(),
-        comments);
+        comments,
+        readOnlyUntil);
   }
 
   private PatchSet.Id buildCurrentPatchSetId() {
@@ -369,6 +375,10 @@
       // behavior.
     }
 
+    if (readOnlyUntil == null) {
+      parseReadOnlyUntil(commit);
+    }
+
     if (lastUpdatedOn == null || ts.after(lastUpdatedOn)) {
       lastUpdatedOn = ts;
     }
@@ -900,6 +910,20 @@
     }
   }
 
+  private void parseReadOnlyUntil(ChangeNotesCommit commit) throws ConfigInvalidException {
+    String raw = parseOneFooter(commit, FOOTER_READ_ONLY_UNTIL);
+    if (raw == null) {
+      return;
+    }
+    try {
+      readOnlyUntil = new Timestamp(GitDateParser.parse(raw, null, Locale.US).getTime());
+    } catch (ParseException e) {
+      ConfigInvalidException cie = invalidFooter(FOOTER_READ_ONLY_UNTIL, raw);
+      cie.initCause(e);
+      throw cie;
+    }
+  }
+
   private void pruneReviewers() {
     Iterator<Table.Cell<Account.Id, ReviewerStateInternal, Timestamp>> rit =
         reviewers.cellSet().iterator();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 62df01b..7b25bbd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -70,7 +70,8 @@
         ImmutableList.of(),
         ImmutableList.of(),
         ImmutableListMultimap.of(),
-        ImmutableListMultimap.of());
+        ImmutableListMultimap.of(),
+        null);
   }
 
   static ChangeNotesState create(
@@ -98,7 +99,8 @@
       List<SubmitRecord> submitRecords,
       List<ChangeMessage> allChangeMessages,
       ListMultimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet,
-      ListMultimap<RevId, Comment> publishedComments) {
+      ListMultimap<RevId, Comment> publishedComments,
+      @Nullable Timestamp readOnlyUntil) {
     if (hashtags == null) {
       hashtags = ImmutableSet.of();
     }
@@ -128,7 +130,8 @@
         ImmutableList.copyOf(submitRecords),
         ImmutableList.copyOf(allChangeMessages),
         ImmutableListMultimap.copyOf(changeMessagesByPatchSet),
-        ImmutableListMultimap.copyOf(publishedComments));
+        ImmutableListMultimap.copyOf(publishedComments),
+        readOnlyUntil);
   }
 
   /**
@@ -206,6 +209,9 @@
 
   abstract ImmutableListMultimap<RevId, Comment> publishedComments();
 
+  @Nullable
+  abstract Timestamp readOnlyUntil();
+
   Change newChange(Project.NameKey project) {
     ChangeColumns c = checkNotNull(columns(), "columns are required");
     Change change =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index c241965..7af0cb4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -29,6 +29,7 @@
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_LABEL;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET_DESCRIPTION;
+import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_READ_ONLY_UNTIL;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_REAL_USER;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_STATUS;
 import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_SUBJECT;
@@ -58,6 +59,7 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.util.LabelVote;
@@ -67,6 +69,7 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Comparator;
 import java.util.Date;
@@ -79,6 +82,7 @@
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -144,12 +148,14 @@
   private boolean isAllowWriteToNewtRef;
   private String psDescription;
   private boolean currentPatchSet;
+  private Timestamp readOnlyUntil;
 
   private ChangeDraftUpdate draftUpdate;
   private RobotCommentUpdate robotCommentUpdate;
 
   @AssistedInject
   private ChangeUpdate(
+      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
       @AnonymousCowardName String anonymousCowardName,
       NotesMigration migration,
@@ -161,6 +167,7 @@
       @Assisted ChangeControl ctl,
       ChangeNoteUtil noteUtil) {
     this(
+        cfg,
         serverIdent,
         anonymousCowardName,
         migration,
@@ -176,6 +183,7 @@
 
   @AssistedInject
   private ChangeUpdate(
+      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
       @AnonymousCowardName String anonymousCowardName,
       NotesMigration migration,
@@ -188,6 +196,7 @@
       @Assisted Date when,
       ChangeNoteUtil noteUtil) {
     this(
+        cfg,
         serverIdent,
         anonymousCowardName,
         migration,
@@ -212,6 +221,7 @@
 
   @AssistedInject
   private ChangeUpdate(
+      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
       @AnonymousCowardName String anonymousCowardName,
       NotesMigration migration,
@@ -223,7 +233,7 @@
       @Assisted Date when,
       @Assisted Comparator<String> labelNameComparator,
       ChangeNoteUtil noteUtil) {
-    super(migration, ctl, serverIdent, anonymousCowardName, noteUtil, when);
+    super(cfg, migration, ctl, serverIdent, anonymousCowardName, noteUtil, when);
     this.accountCache = accountCache;
     this.draftUpdateFactory = draftUpdateFactory;
     this.robotCommentUpdateFactory = robotCommentUpdateFactory;
@@ -233,6 +243,7 @@
 
   @AssistedInject
   private ChangeUpdate(
+      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
       @AnonymousCowardName String anonymousCowardName,
       NotesMigration migration,
@@ -248,6 +259,7 @@
       @Assisted Date when,
       @Assisted Comparator<String> labelNameComparator) {
     super(
+        cfg,
         migration,
         noteUtil,
         serverIdent,
@@ -695,6 +707,10 @@
       addIdent(msg, realAccountId).append('\n');
     }
 
+    if (readOnlyUntil != null) {
+      addFooter(msg, FOOTER_READ_ONLY_UNTIL, ChangeNoteUtil.formatTime(serverIdent, readOnlyUntil));
+    }
+
     cb.setMessage(msg.toString());
     try {
       ObjectId treeId = storeRevisionNotes(rw, ins, curr);
@@ -740,7 +756,8 @@
         && groups == null
         && tag == null
         && psDescription == null
-        && !currentPatchSet;
+        && !currentPatchSet
+        && readOnlyUntil == null;
   }
 
   ChangeDraftUpdate getDraftUpdate() {
@@ -760,6 +777,10 @@
     return isAllowWriteToNewtRef;
   }
 
+  void setReadOnlyUntil(Timestamp readOnlyUntil) {
+    this.readOnlyUntil = readOnlyUntil;
+  }
+
   private static StringBuilder addFooter(StringBuilder sb, FooterKey footer) {
     return sb.append(footer.getName()).append(": ");
   }
@@ -782,4 +803,13 @@
     sb.append('>');
     return sb;
   }
+
+  @Override
+  protected void checkNotReadOnly() throws OrmException {
+    // Allow setting Read-only-until to 0 to release an existing lease.
+    if (readOnlyUntil != null && readOnlyUntil.getTime() == 0) {
+      return;
+    }
+    super.checkNotReadOnly();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
index 1350a1f..fef7fdf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbChangeState.java
@@ -301,7 +301,7 @@
     return cfg.getTimeUnit("notedb", null, "maxTimestampSkew", 1000, TimeUnit.MILLISECONDS);
   }
 
-  private static Timestamp timeForReadOnlyCheck(long skewMs) {
+  static Timestamp timeForReadOnlyCheck(long skewMs) {
     // Subtract some slop in case the machine that set the change's read-only
     // lease has a clock behind ours.
     return new Timestamp(TimeUtil.nowMs() - skewMs);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
index 0b01e14..d249689 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbModule.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.config.FactoryModule;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
@@ -95,6 +96,11 @@
                 public void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle) {
                   // Do nothing.
                 }
+
+                @Override
+                public void rebuildReviewDb(ReviewDb db, Project.NameKey project, Id changeId) {
+                  // Do nothing.
+                }
               });
       bind(new TypeLiteral<Cache<ChangeNotesCache.Key, ChangeNotesState>>() {})
           .annotatedWith(Names.named(ChangeNotesCache.CACHE_NAME))
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
index cc738e5..37b730f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NoteDbUpdateManager.java
@@ -527,7 +527,7 @@
     private MismatchedStateException(Change.Id id, NoteDbChangeState expectedState) {
       super(
           String.format(
-              "cannot apply NoteDb updates for change %s;" + " change meta ref does not match %s",
+              "cannot apply NoteDb updates for change %s; change meta ref does not match %s",
               id, expectedState.getChangeMetaId().name()));
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java
index 10345c8..0995c2d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/PrimaryStorageMigrator.java
@@ -26,18 +26,32 @@
 import com.github.rholder.retry.WaitStrategies;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Stopwatch;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.TimeUtil;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
+import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.BatchUpdate;
+import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.RepoRefCache;
+import com.google.gerrit.server.git.UpdateException;
+import com.google.gerrit.server.index.change.ChangeField;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+import com.google.gerrit.server.notedb.NoteDbChangeState.RefState;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.InternalChangeQuery;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.OrmRuntimeException;
@@ -46,11 +60,17 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.sql.Timestamp;
+import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeSet;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -60,10 +80,15 @@
 public class PrimaryStorageMigrator {
   private static final Logger log = LoggerFactory.getLogger(PrimaryStorageMigrator.class);
 
-  private final Provider<ReviewDb> db;
-  private final GitRepositoryManager repoManager;
   private final AllUsersName allUsers;
+  private final BatchUpdate.Factory batchUpdateFactory;
+  private final ChangeControl.GenericFactory changeControlFactory;
   private final ChangeRebuilder rebuilder;
+  private final ChangeUpdate.Factory updateFactory;
+  private final GitRepositoryManager repoManager;
+  private final InternalUser.Factory internalUserFactory;
+  private final Provider<InternalChangeQuery> queryProvider;
+  private final Provider<ReviewDb> db;
 
   private final long skewMs;
   private final long timeoutMs;
@@ -75,8 +100,24 @@
       Provider<ReviewDb> db,
       GitRepositoryManager repoManager,
       AllUsersName allUsers,
-      ChangeRebuilder rebuilder) {
-    this(cfg, db, repoManager, allUsers, rebuilder, null);
+      ChangeRebuilder rebuilder,
+      ChangeControl.GenericFactory changeControlFactory,
+      Provider<InternalChangeQuery> queryProvider,
+      ChangeUpdate.Factory updateFactory,
+      InternalUser.Factory internalUserFactory,
+      BatchUpdate.Factory batchUpdateFactory) {
+    this(
+        cfg,
+        db,
+        repoManager,
+        allUsers,
+        rebuilder,
+        null,
+        changeControlFactory,
+        queryProvider,
+        updateFactory,
+        internalUserFactory,
+        batchUpdateFactory);
   }
 
   @VisibleForTesting
@@ -86,12 +127,22 @@
       GitRepositoryManager repoManager,
       AllUsersName allUsers,
       ChangeRebuilder rebuilder,
-      @Nullable Retryer<NoteDbChangeState> testEnsureRebuiltRetryer) {
+      @Nullable Retryer<NoteDbChangeState> testEnsureRebuiltRetryer,
+      ChangeControl.GenericFactory changeControlFactory,
+      Provider<InternalChangeQuery> queryProvider,
+      ChangeUpdate.Factory updateFactory,
+      InternalUser.Factory internalUserFactory,
+      BatchUpdate.Factory batchUpdateFactory) {
     this.db = db;
     this.repoManager = repoManager;
     this.allUsers = allUsers;
     this.rebuilder = rebuilder;
     this.testEnsureRebuiltRetryer = testEnsureRebuiltRetryer;
+    this.changeControlFactory = changeControlFactory;
+    this.queryProvider = queryProvider;
+    this.updateFactory = updateFactory;
+    this.internalUserFactory = internalUserFactory;
+    this.batchUpdateFactory = batchUpdateFactory;
     skewMs = NoteDbChangeState.getReadOnlySkew(cfg);
 
     String s = "notedb";
@@ -189,7 +240,7 @@
     //   failure.
 
     Stopwatch sw = Stopwatch.createStarted();
-    Change readOnlyChange = setReadOnly(id); // MRO
+    Change readOnlyChange = setReadOnlyInReviewDb(id); // MRO
     if (readOnlyChange == null) {
       return; // Already migrated.
     }
@@ -217,7 +268,7 @@
     log.info("Migrated change {} to NoteDb primary in {}ms", id, sw.elapsed(MILLISECONDS));
   }
 
-  private Change setReadOnly(Change.Id id) throws OrmException {
+  private Change setReadOnlyInReviewDb(Change.Id id) throws OrmException {
     AtomicBoolean alreadyMigrated = new AtomicBoolean(false);
     Change result =
         db().changes()
@@ -316,4 +367,124 @@
   private String badState(NoteDbChangeState actual, NoteDbChangeState expected) {
     return "state changed unexpectedly: " + actual + " != " + expected;
   }
+
+  public void migrateToReviewDbPrimary(Change.Id id, @Nullable Project.NameKey project)
+      throws OrmException, IOException {
+    // Migrating back to ReviewDb primary is much simpler than the original migration to NoteDb
+    // primary, because when NoteDb is primary, each write only goes to one storage location rather
+    // than both. We only need to consider whether a concurrent writer (OR) conflicts with the first
+    // setReadOnlyInNoteDb step (MR) in this method.
+    //
+    // If OR wins, then either:
+    // * MR will set read-only after OR is completed, which is not a concurrent write.
+    // * MR will fail to set read-only with a lock failure. The caller will have to retry, but the
+    //   change is not in a read-only state, so behavior is not degraded in the meantime.
+    //
+    // If MR wins, then either:
+    // * OR will fail with a read-only exception (via AbstractChangeNotes#apply).
+    // * OR will fail with a lock failure.
+    //
+    // In all of these scenarios, the change is read-only if and only if MR succeeds.
+    //
+    // There will be no concurrent writes to ReviewDb for this change until
+    // setPrimaryStorageReviewDb completes, because ReviewDb writes are not attempted when primary
+    // storage is NoteDb. After the primary storage changes back, it is possible for subsequent
+    // NoteDb writes to conflict with the releaseReadOnlyLeaseInNoteDb step, but at this point,
+    // since ReviewDb is primary, we are back to ignoring them.
+    Stopwatch sw = Stopwatch.createStarted();
+    if (project == null) {
+      project = getProject(id);
+    }
+    ObjectId newMetaId = setReadOnlyInNoteDb(project, id);
+    rebuilder.rebuildReviewDb(db(), project, id);
+    setPrimaryStorageReviewDb(id, newMetaId);
+    releaseReadOnlyLeaseInNoteDb(project, id);
+    log.info("Migrated change {} to ReviewDb primary in {}ms", id, sw.elapsed(MILLISECONDS));
+  }
+
+  private ObjectId setReadOnlyInNoteDb(Project.NameKey project, Change.Id id)
+      throws OrmException, IOException {
+    Timestamp now = TimeUtil.nowTs();
+    Timestamp until = new Timestamp(now.getTime() + timeoutMs);
+    ChangeUpdate update =
+        updateFactory.create(
+            changeControlFactory.controlFor(db.get(), project, id, internalUserFactory.create()));
+    update.setReadOnlyUntil(until);
+    return update.commit();
+  }
+
+  private void setPrimaryStorageReviewDb(Change.Id id, ObjectId newMetaId)
+      throws OrmException, IOException {
+    ImmutableMap.Builder<Account.Id, ObjectId> draftIds = ImmutableMap.builder();
+    try (Repository repo = repoManager.openRepository(allUsers)) {
+      for (Ref draftRef :
+          repo.getRefDatabase().getRefs(RefNames.refsDraftCommentsPrefix(id)).values()) {
+        Account.Id accountId = Account.Id.fromRef(draftRef.getName());
+        if (accountId != null) {
+          draftIds.put(accountId, draftRef.getObjectId().copy());
+        }
+      }
+    }
+    NoteDbChangeState newState =
+        new NoteDbChangeState(
+            id,
+            PrimaryStorage.REVIEW_DB,
+            Optional.of(RefState.create(newMetaId, draftIds.build())),
+            Optional.empty());
+    db().changes()
+        .atomicUpdate(
+            id,
+            new AtomicUpdate<Change>() {
+              @Override
+              public Change update(Change change) {
+                if (PrimaryStorage.of(change) != PrimaryStorage.NOTE_DB) {
+                  throw new OrmRuntimeException(
+                      "change " + id + " is not NoteDb primary: " + change.getNoteDbState());
+                }
+                change.setNoteDbState(newState.toString());
+                return change;
+              }
+            });
+  }
+
+  private void releaseReadOnlyLeaseInNoteDb(Project.NameKey project, Change.Id id)
+      throws OrmException {
+    // Use a BatchUpdate since ReviewDb is primary at this point, so it needs to reflect the update.
+    try (BatchUpdate bu =
+        batchUpdateFactory.create(
+            db.get(), project, internalUserFactory.create(), TimeUtil.nowTs())) {
+      bu.addOp(
+          id,
+          new BatchUpdate.Op() {
+            @Override
+            public boolean updateChange(ChangeContext ctx) {
+              ctx.getUpdate(ctx.getChange().currentPatchSetId()).setReadOnlyUntil(new Timestamp(0));
+              return true;
+            }
+          });
+      bu.execute();
+    } catch (RestApiException | UpdateException e) {
+      throw new OrmException(e);
+    }
+  }
+
+  private Project.NameKey getProject(Change.Id id) throws OrmException {
+    List<ChangeData> cds =
+        queryProvider
+            .get()
+            .setRequestedFields(ImmutableSet.of(ChangeField.PROJECT.getName()))
+            .byLegacyChangeId(id);
+    Set<Project.NameKey> projects = new TreeSet<>();
+    for (ChangeData cd : cds) {
+      projects.add(cd.project());
+    }
+    if (projects.size() != 1) {
+      throw new OrmException(
+          "zero or multiple projects found for change "
+              + id
+              + ", must specify project explicitly: "
+              + projects);
+    }
+    return projects.iterator().next();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
index 9ce4b54..82593eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.reviewdb.client.RobotComment;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
@@ -38,6 +39,7 @@
 import java.util.Set;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
@@ -73,6 +75,7 @@
 
   @AssistedInject
   private RobotCommentUpdate(
+      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
       @AnonymousCowardName String anonymousCowardName,
       NotesMigration migration,
@@ -83,6 +86,7 @@
       @Assisted PersonIdent authorIdent,
       @Assisted Date when) {
     super(
+        cfg,
         migration,
         noteUtil,
         serverIdent,
@@ -97,6 +101,7 @@
 
   @AssistedInject
   private RobotCommentUpdate(
+      @GerritServerConfig Config cfg,
       @GerritPersonIdent PersonIdent serverIdent,
       @AnonymousCowardName String anonymousCowardName,
       NotesMigration migration,
@@ -107,6 +112,7 @@
       @Assisted PersonIdent authorIdent,
       @Assisted Date when) {
     super(
+        cfg,
         migration,
         noteUtil,
         serverIdent,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java
index 4ee84dc..11fef24 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/TestChangeRebuilderWrapper.java
@@ -16,6 +16,8 @@
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
@@ -111,4 +113,13 @@
     // Don't check for manual failure; that happens in execute().
     delegate.buildUpdates(manager, bundle);
   }
+
+  @Override
+  public void rebuildReviewDb(ReviewDb db, Project.NameKey project, Id changeId)
+      throws OrmException {
+    if (failNextUpdate.getAndSet(false)) {
+      throw new OrmException("Update failed");
+    }
+    delegate.rebuildReviewDb(db, project, changeId);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java
index 54303fc..ad22330 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeMessageEvent.java
@@ -32,7 +32,7 @@
           Change.Status.ABANDONED, Pattern.compile("^Abandoned(\n.*)*$"),
           Change.Status.MERGED,
               Pattern.compile(
-                  "^Change has been successfully" + " (merged|cherry-picked|rebased|pushed).*$"),
+                  "^Change has been successfully (merged|cherry-picked|rebased|pushed).*$"),
           Change.Status.NEW, Pattern.compile("^Restored(\n.*)*$"));
 
   private static final Pattern TOPIC_SET_REGEXP = Pattern.compile("^Topic set to (.+)$");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java
index c8d9bb3..6f9090f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilder.java
@@ -17,6 +17,7 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.notedb.ChangeBundle;
 import com.google.gerrit.server.notedb.NoteDbUpdateManager;
@@ -54,6 +55,17 @@
         });
   }
 
+  /**
+   * Rebuild ReviewDb contents by copying from NoteDb.
+   *
+   * <p>Requires NoteDb to be the primary storage for the change.
+   */
+  public abstract void rebuildReviewDb(ReviewDb db, Project.NameKey project, Change.Id changeId)
+      throws OrmException;
+
+  // In the following methods "rebuilding" always refers to copying the state
+  // from ReviewDb to NoteDb, i.e. assuming ReviewDb is the primary storage.
+
   public abstract Result rebuild(ReviewDb db, Change.Id changeId) throws IOException, OrmException;
 
   public abstract Result rebuildEvenIfReadOnly(ReviewDb db, Change.Id changeId)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
index 10f4dd8..4ba3a88 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
@@ -23,6 +23,7 @@
 import static java.util.stream.Collectors.toList;
 
 import com.google.common.base.Splitter;
+import com.google.common.collect.FluentIterable;
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.ListMultimap;
@@ -41,6 +42,7 @@
 import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
@@ -67,6 +69,8 @@
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
+import com.google.gwtorm.client.Key;
+import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
@@ -74,6 +78,7 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
@@ -112,7 +117,9 @@
   private final ChangeBundleReader bundleReader;
   private final ChangeDraftUpdate.Factory draftUpdateFactory;
   private final ChangeNoteUtil changeNoteUtil;
+  private final ChangeNotes.Factory notesFactory;
   private final ChangeUpdate.Factory updateFactory;
+  private final CommentsUtil commentsUtil;
   private final NoteDbUpdateManager.Factory updateManagerFactory;
   private final NotesMigration migration;
   private final PatchListCache patchListCache;
@@ -130,7 +137,9 @@
       ChangeBundleReader bundleReader,
       ChangeDraftUpdate.Factory draftUpdateFactory,
       ChangeNoteUtil changeNoteUtil,
+      ChangeNotes.Factory notesFactory,
       ChangeUpdate.Factory updateFactory,
+      CommentsUtil commentsUtil,
       NoteDbUpdateManager.Factory updateManagerFactory,
       NotesMigration migration,
       PatchListCache patchListCache,
@@ -143,7 +152,9 @@
     this.bundleReader = bundleReader;
     this.draftUpdateFactory = draftUpdateFactory;
     this.changeNoteUtil = changeNoteUtil;
+    this.notesFactory = notesFactory;
     this.updateFactory = updateFactory;
+    this.commentsUtil = commentsUtil;
     this.updateManagerFactory = updateManagerFactory;
     this.migration = migration;
     this.patchListCache = patchListCache;
@@ -613,4 +624,45 @@
     update.setBranch(change.getDest().get());
     update.setSubject(change.getOriginalSubject());
   }
+
+  @Override
+  public void rebuildReviewDb(ReviewDb db, Project.NameKey project, Change.Id changeId)
+      throws OrmException {
+    // TODO(dborowitz): Fail fast if changes tables are disabled in ReviewDb.
+    ChangeNotes notes = notesFactory.create(db, project, changeId);
+    ChangeBundle bundle = ChangeBundle.fromNotes(commentsUtil, notes);
+
+    db = ReviewDbUtil.unwrapDb(db);
+    db.changes().beginTransaction(changeId);
+    try {
+      Change c = db.changes().get(changeId);
+      PrimaryStorage ps = PrimaryStorage.of(c);
+      if (ps != PrimaryStorage.NOTE_DB) {
+        throw new OrmException("primary storage of " + changeId + " is " + ps);
+      }
+      db.changes().upsert(Collections.singleton(c));
+      putExactlyEntities(
+          db.changeMessages(), db.changeMessages().byChange(c.getId()), bundle.getChangeMessages());
+      putExactlyEntities(db.patchSets(), db.patchSets().byChange(c.getId()), bundle.getPatchSets());
+      putExactlyEntities(
+          db.patchSetApprovals(),
+          db.patchSetApprovals().byChange(c.getId()),
+          bundle.getPatchSetApprovals());
+      putExactlyEntities(
+          db.patchComments(),
+          db.patchComments().byChange(c.getId()),
+          bundle.getPatchLineComments());
+      db.commit();
+    } finally {
+      db.rollback();
+    }
+  }
+
+  private static <T, K extends Key<?>> void putExactlyEntities(
+      Access<T, K> access, Iterable<T> existing, Collection<T> ents) throws OrmException {
+    Set<K> toKeep = access.toMap(ents).keySet();
+    access.delete(
+        FluentIterable.from(existing).filter(e -> !toKeep.contains(access.primaryKey(e))));
+    access.upsert(ents);
+  }
 }
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 9183579..957bdd7 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
@@ -194,7 +194,7 @@
     if (!originalName.equals(name)) {
       log.warn(
           String.format(
-              "Plugin provides its own name: <%s>," + " use it instead of the input name: <%s>",
+              "Plugin provides its own name: <%s>, use it instead of the input name: <%s>",
               name, originalName));
     }
 
@@ -255,8 +255,7 @@
 
   public void disablePlugins(Set<String> names) {
     if (!isRemoteAdminEnabled()) {
-      log.warn(
-          "Remote plugin administration is disabled," + " ignoring disablePlugins(" + names + ")");
+      log.warn("Remote plugin administration is disabled, ignoring disablePlugins(" + names + ")");
       return;
     }
 
@@ -296,8 +295,7 @@
 
   public void enablePlugins(Set<String> names) throws PluginInstallException {
     if (!isRemoteAdminEnabled()) {
-      log.warn(
-          "Remote plugin administration is disabled," + " ignoring enablePlugins(" + names + ")");
+      log.warn("Remote plugin administration is disabled, ignoring enablePlugins(" + names + ")");
       return;
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java
index b704fc8..f824f81 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CheckMergeability.java
@@ -61,7 +61,7 @@
   @Option(
     name = "--strategy",
     metaVar = "STRATEGY",
-    usage = "name of the merge strategy, refer to " + "org.eclipse.jgit.merge.MergeStrategy"
+    usage = "name of the merge strategy, refer to org.eclipse.jgit.merge.MergeStrategy"
   )
   public void setStrategy(String strategy) {
     this.strategy = strategy;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
index b36a09d..c74efc6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetAccess.java
@@ -107,7 +107,7 @@
           checkGlobalCapabilityPermissions(config.getName());
         } else if (!projectControl.controlForRef(section.getName()).isOwner()) {
           throw new AuthException(
-              "You are not allowed to edit permissions" + "for ref: " + section.getName());
+              "You are not allowed to edit permissionsfor ref: " + section.getName());
         }
       }
       // Perform addition checks
@@ -122,7 +122,7 @@
             throw new BadRequestException("invalid section name");
           }
           if (!projectControl.controlForRef(name).isOwner()) {
-            throw new AuthException("You are not allowed to edit permissions" + "for ref: " + name);
+            throw new AuthException("You are not allowed to edit permissionsfor ref: " + name);
           }
           RefPattern.validate(name);
         }
@@ -273,12 +273,12 @@
 
     if (!allProjects.equals(projectName)) {
       throw new BadRequestException(
-          "Cannot edit global capabilities " + "for projects other than " + allProjects.get());
+          "Cannot edit global capabilities for projects other than " + allProjects.get());
     }
 
     if (!identifiedUser.get().getCapabilities().canAdministrateServer()) {
       throw new AuthException(
-          "Editing global capabilities " + "requires " + GlobalCapability.ADMINISTRATE_SERVER);
+          "Editing global capabilities requires " + GlobalCapability.ADMINISTRATE_SERVER);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
index e6ad352..d535062 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -246,7 +246,7 @@
       // whether or not that is actually possible given the permissions.
       return ruleError(
           String.format(
-              "Submit rule '%s' for change %s of %s has " + "no solution.",
+              "Submit rule '%s' for change %s of %s has no solution.",
               getSubmitRuleName(), cd.getId(), getProjectName()));
     }
 
@@ -362,7 +362,7 @@
   private List<SubmitRecord> invalidResult(Term rule, Term record, String reason) {
     return ruleError(
         String.format(
-            "Submit rule %s for change %s of %s output " + "invalid result: %s%s",
+            "Submit rule %s for change %s of %s output invalid result: %s%s",
             rule,
             cd.getId(),
             getProjectName(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
index 7e03355..1bf6d8b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
@@ -86,7 +86,7 @@
     for (Predicate<T> c : children) {
       checkState(
           c.isMatchable(),
-          "match invoked, but child predicate %s " + "doesn't implement %s",
+          "match invoked, but child predicate %s doesn't implement %s",
           c,
           Matchable.class.getName());
       if (!c.asMatchable().match(object)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
index 530dfb9..3716ec1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
@@ -67,7 +67,7 @@
   public boolean match(final T object) throws OrmException {
     checkState(
         that.isMatchable(),
-        "match invoked, but child predicate %s " + "doesn't implement %s",
+        "match invoked, but child predicate %s doesn't implement %s",
         that,
         Matchable.class.getName());
     return !that.asMatchable().match(object);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
index 1dd46f9..4845a86 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
@@ -86,7 +86,7 @@
     for (final Predicate<T> c : children) {
       checkState(
           c.isMatchable(),
-          "match invoked, but child predicate %s " + "doesn't implement %s",
+          "match invoked, but child predicate %s doesn't implement %s",
           c,
           Matchable.class.getName());
       if (c.asMatchable().match(object)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
index b5ea361..5fb4497 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryProcessor.java
@@ -52,8 +52,7 @@
       executionTime =
           metricMaker.newTimer(
               "query/query_latency",
-              new Description(
-                      "Successful query latency," + " accumulated over the life of the process")
+              new Description("Successful query latency, accumulated over the life of the process")
                   .setCumulative()
                   .setUnit(Description.Units.MILLISECONDS),
               index);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 84af7be..787508a7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -668,7 +668,7 @@
     if (project == null) {
       checkState(
           !notesMigration.readChanges(),
-          "should not have created " + " ChangeData without a project when NoteDb is enabled");
+          "should not have created  ChangeData without a project when NoteDb is enabled");
       project = change().getProject();
     }
     return project;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_106.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_106.java
index 294c96d..5bb3669 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_106.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_106.java
@@ -151,7 +151,7 @@
       } catch (IOException e) {
         throw new IOException(
             String.format(
-                "ERROR: Failed to create reflog file for the" + " %s branch in repository %s",
+                "ERROR: Failed to create reflog file for the %s branch in repository %s",
                 RefNames.REFS_CONFIG, project.get()));
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_123.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_123.java
index 5a0c61d..ec63141 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_123.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_123.java
@@ -56,8 +56,7 @@
     ListMultimap<Account.Id, Change.Id> imports =
         MultimapBuilder.hashKeys().arrayListValues().build();
     try (Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-        ResultSet rs =
-            stmt.executeQuery("SELECT " + "account_id, " + "change_id " + "FROM starred_changes")) {
+        ResultSet rs = stmt.executeQuery("SELECT account_id, change_id FROM starred_changes")) {
       while (rs.next()) {
         Account.Id accountId = new Account.Id(rs.getInt(1));
         Change.Id changeId = new Change.Id(rs.getInt(2));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_94.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_94.java
index 5a87562..d4a189f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_94.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_94.java
@@ -29,7 +29,7 @@
   @Override
   protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
     try (Statement stmt = newStatement(db)) {
-      stmt.execute("CREATE INDEX patch_sets_byRevision" + " ON patch_sets (revision)");
+      stmt.execute("CREATE INDEX patch_sets_byRevision ON patch_sets (revision)");
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_98.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_98.java
index 4206dce..eec3c9f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_98.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_98.java
@@ -28,7 +28,7 @@
 
   @Override
   protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    ui.message("Migrate user preference showUserInReview to " + "reviewCategoryStrategy");
+    ui.message("Migrate user preference showUserInReview to reviewCategoryStrategy");
     try (Statement stmt = newStatement(db)) {
       stmt.executeUpdate(
           "UPDATE accounts SET "
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
index 28f10d9..fa4a951 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
@@ -71,7 +71,7 @@
     PrologEnvironment env = envFactory.create(machine);
     setUpEnvironment(env);
 
-    String script = "loopy :- b(5).\n" + "b(N) :- N > 0, !, S = N - 1, b(S).\n" + "b(_) :- true.\n";
+    String script = "loopy :- b(5).\nb(N) :- N > 0, !, S = N - 1, b(S).\nb(_) :- true.\n";
 
     SymbolTerm nameTerm = SymbolTerm.create("testReductionLimit");
     JavaObjectTerm inTerm =
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java
index 96bac3c..780ac71 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/change/HashtagsTest.java
@@ -87,7 +87,7 @@
 
   @Test
   public void hashtagsWithAccentedCharacters() throws Exception {
-    String commitMessage = "Jag #måste #öva på min #Svenska!\n\n" + "Jag behöver en #läkare.";
+    String commitMessage = "Jag #måste #öva på min #Svenska!\n\nJag behöver en #läkare.";
     assertThat(HashtagsUtil.extractTags(commitMessage))
         .containsExactlyElementsIn(Sets.newHashSet("måste", "öva", "Svenska", "läkare"));
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
index d4577c5..ab68c10 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -199,7 +199,7 @@
     ProjectConfig cfg = read(rev);
     assertThat(cfg.getValidationErrors()).hasSize(1);
     assertThat(Iterables.getOnlyElement(cfg.getValidationErrors()).getMessage())
-        .isEqualTo("project.config: Invalid defaultValue \"-2\" " + "for label \"CustomLabel\"");
+        .isEqualTo("project.config: Invalid defaultValue \"-2\" for label \"CustomLabel\"");
   }
 
   @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java
index 76995d5..0c4507e 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/MetadataParserTest.java
@@ -62,11 +62,11 @@
     b.subject("");
 
     StringBuilder stringBuilder = new StringBuilder();
-    stringBuilder.append(toFooterWithDelimiter(MetadataName.CHANGE_ID) + "cid" + "\r\n");
-    stringBuilder.append("> " + toFooterWithDelimiter(MetadataName.PATCH_SET) + "1" + "\n");
-    stringBuilder.append(toFooterWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment" + "\n");
+    stringBuilder.append(toFooterWithDelimiter(MetadataName.CHANGE_ID) + "cid\r\n");
+    stringBuilder.append("> " + toFooterWithDelimiter(MetadataName.PATCH_SET) + "1\n");
+    stringBuilder.append(toFooterWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment\n");
     stringBuilder.append(
-        toFooterWithDelimiter(MetadataName.TIMESTAMP) + "Tue, 25 Oct 2016 02:11:35 -0700" + "\r\n");
+        toFooterWithDelimiter(MetadataName.TIMESTAMP) + "Tue, 25 Oct 2016 02:11:35 -0700\r\n");
     b.textContent(stringBuilder.toString());
 
     Address author = new Address("Diffy", "test@gerritcodereview.com");
@@ -92,10 +92,10 @@
 
     StringBuilder stringBuilder = new StringBuilder();
     stringBuilder.append(
-        "<div id\"someid\">" + toFooterWithDelimiter(MetadataName.CHANGE_ID) + "cid" + "</div>");
-    stringBuilder.append("<div>" + toFooterWithDelimiter(MetadataName.PATCH_SET) + "1" + "</div>");
+        "<div id\"someid\">" + toFooterWithDelimiter(MetadataName.CHANGE_ID) + "cid</div>");
+    stringBuilder.append("<div>" + toFooterWithDelimiter(MetadataName.PATCH_SET) + "1</div>");
     stringBuilder.append(
-        "<div>" + toFooterWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment" + "</div>");
+        "<div>" + toFooterWithDelimiter(MetadataName.MESSAGE_TYPE) + "comment</div>");
     stringBuilder.append(
         "<div>"
             + toFooterWithDelimiter(MetadataName.TIMESTAMP)
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java
index 2c418ec..d8530b5 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/AttachmentMessage.java
@@ -72,7 +72,7 @@
     System.out.println("\uD83D\uDE1B test");
     MailMessage.Builder expect = MailMessage.builder();
     expect
-        .id("<CAM7sg=3meaAVUxW3KXeJEVs8sv_ADw1BnvpcHHiYVR2TQQi__w" + "@mail.gmail.com>")
+        .id("<CAM7sg=3meaAVUxW3KXeJEVs8sv_ADw1BnvpcHHiYVR2TQQi__w@mail.gmail.com>")
         .from(new Address("Patrick Hiesel", "hiesel@google.com"))
         .addTo(new Address("Patrick Hiesel", "hiesel@google.com"))
         .textContent("Contains unwanted attachment")
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java
index c19e618..487e9dd 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/HtmlMimeMessage.java
@@ -96,7 +96,7 @@
         .addTo(new Address("Patrick Hiesel", "hiesel@google.com"))
         .textContent(textContent)
         .htmlContent(unencodedHtmlContent)
-        .subject("Change in gerrit[master]: Implement " + "receiver class structure and bindings")
+        .subject("Change in gerrit[master]: Implement receiver class structure and bindings")
         .addAdditionalHeader("MIME-Version: 1.0")
         .dateReceived(new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC));
     return expect.build();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java
index 8fc1dbc..ce833d5 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/receive/data/SimpleTextMessage.java
@@ -123,10 +123,10 @@
         .addCc(new Address("Jonathan Nieder", "jrn@google.com"))
         .addCc(new Address("Patrick Hiesel", "hiesel@google.com"))
         .textContent(textContent)
-        .subject("Change in gerrit[master]: (Re)enable voting" + " buttons for merged changes")
+        .subject("Change in gerrit[master]: (Re)enable voting buttons for merged changes")
         .dateReceived(new DateTime(2016, 10, 25, 9, 11, 35, 0, DateTimeZone.UTC))
         .addAdditionalHeader(
-            "Authentication-Results: mx.google.com; " + "dkim=pass header.i=@google.com;")
+            "Authentication-Results: mx.google.com; dkim=pass header.i=@google.com;")
         .addAdditionalHeader(
             "In-Reply-To: <gerrit.1477487889000.Iba501e00bee"
                 + "77be3bd0ced72f88fd04ba0accaed@gerrit-review.googlesource.com>")
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
index 916ba9f..90e6800 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeBundleTest.java
@@ -121,7 +121,7 @@
         b1,
         b2,
         "changeId differs for Changes: {" + id1 + "} != {" + id2 + "}",
-        "createdOn differs for Changes:" + " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}",
+        "createdOn differs for Changes: {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}",
         "effective last updated time differs for Changes:"
             + " {2009-09-30 17:00:00.0} != {2009-09-30 17:00:06.0}");
   }
@@ -310,7 +310,7 @@
     ChangeBundle b2 =
         new ChangeBundle(
             c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ":" + " {} != {null}");
+    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": {} != {null}");
 
     // Topic ignored if ReviewDb is empty and NoteDb is null.
     b1 =
@@ -328,7 +328,7 @@
     b2 =
         new ChangeBundle(
             c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ":" + " {} != {null}");
+    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": {} != {null}");
 
     // Null is not equal to a non-empty string.
     Change c3 = clone(c1);
@@ -339,7 +339,7 @@
     b2 =
         new ChangeBundle(
             c2, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ":" + " {topic} != {null}");
+    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": {topic} != {null}");
 
     // Null is equal to a string that is all whitespace.
     Change c4 = clone(c1);
@@ -368,7 +368,7 @@
     ChangeBundle b2 =
         new ChangeBundle(
             c2, messages(), patchSets(), approvals(), comments(), reviewers(), REVIEW_DB);
-    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ":" + " { abc } != {abc}");
+    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": { abc } != {abc}");
 
     // Leading whitespace in ReviewDb topic is ignored.
     b1 =
@@ -389,7 +389,7 @@
     b2 =
         new ChangeBundle(
             c3, messages(), patchSets(), approvals(), comments(), reviewers(), NOTE_DB);
-    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ":" + " { abc } != {cba}");
+    assertDiffs(b1, b2, "topic differs for Change.Id " + c1.getId() + ": { abc } != {cba}");
   }
 
   @Test
@@ -492,7 +492,7 @@
     assertDiffs(
         b1,
         b2,
-        "subject differs for Change.Id " + c1.getId() + ":" + " {Change subject} != {Change sub}");
+        "subject differs for Change.Id " + c1.getId() + ": {Change subject} != {Change sub}");
 
     // ReviewDb has shorter subject, allowed.
     b1 =
@@ -512,7 +512,7 @@
     assertDiffs(
         b1,
         b2,
-        "subject differs for Change.Id " + c1.getId() + ":" + " {Change subject} != {Change sub}");
+        "subject differs for Change.Id " + c1.getId() + ": {Change subject} != {Change sub}");
   }
 
   @Test
@@ -645,7 +645,7 @@
     assertDiffs(
         b1,
         b2,
-        "currentPatchSetId differs for Change.Id " + c1.getId() + ":" + " {1} != {0}",
+        "currentPatchSetId differs for Change.Id " + c1.getId() + ": {1} != {0}",
         "subject differs for Change.Id "
             + c1.getId()
             + ":"
@@ -807,10 +807,8 @@
     b2 =
         new ChangeBundle(
             c, messages(cm1), latest(c), approvals(), comments(), reviewers(), NOTE_DB);
-    assertDiffs(
-        b1, b2, "ChangeMessages differ for Change.Id " + id + "\n" + "Only in A:\n  " + cm2);
-    assertDiffs(
-        b2, b1, "ChangeMessages differ for Change.Id " + id + "\n" + "Only in B:\n  " + cm2);
+    assertDiffs(b1, b2, "ChangeMessages differ for Change.Id " + id + "\nOnly in A:\n  " + cm2);
+    assertDiffs(b2, b1, "ChangeMessages differ for Change.Id " + id + "\nOnly in B:\n  " + cm2);
   }
 
   @Test
@@ -1028,8 +1026,7 @@
         new ChangeBundle(
             c, messages(), patchSets(ps1, ps2), approvals(), comments(), reviewers(), REVIEW_DB);
 
-    assertDiffs(
-        b1, b2, "PatchSet.Id sets differ:" + " [] only in A; [" + c.getId() + ",1] only in B");
+    assertDiffs(b1, b2, "PatchSet.Id sets differ: [] only in A; [" + c.getId() + ",1] only in B");
   }
 
   @Test
@@ -1200,8 +1197,8 @@
         b1,
         b2,
         "ChangeMessage.Key sets differ: [] only in A; [" + cm2.getKey() + "] only in B",
-        "PatchSet.Id sets differ:" + " [] only in A; [" + ps2.getId() + "] only in B",
-        "PatchSetApproval.Key sets differ:" + " [] only in A; [" + a2.getKey() + "] only in B");
+        "PatchSet.Id sets differ: [] only in A; [" + ps2.getId() + "] only in B",
+        "PatchSetApproval.Key sets differ: [] only in A; [" + a2.getKey() + "] only in B");
 
     // One NoteDb.
     b1 =
@@ -1219,9 +1216,9 @@
     assertDiffs(
         b1,
         b2,
-        "ChangeMessages differ for Change.Id " + c.getId() + "\n" + "Only in B:\n  " + cm2,
-        "PatchSet.Id sets differ:" + " [] only in A; [" + ps2.getId() + "] only in B",
-        "PatchSetApproval.Key sets differ:" + " [] only in A; [" + a2.getKey() + "] only in B");
+        "ChangeMessages differ for Change.Id " + c.getId() + "\nOnly in B:\n  " + cm2,
+        "PatchSet.Id sets differ: [] only in A; [" + ps2.getId() + "] only in B",
+        "PatchSetApproval.Key sets differ: [] only in A; [" + a2.getKey() + "] only in B");
 
     // Both NoteDb.
     b1 =
@@ -1239,9 +1236,9 @@
     assertDiffs(
         b1,
         b2,
-        "ChangeMessages differ for Change.Id " + c.getId() + "\n" + "Only in B:\n  " + cm2,
-        "PatchSet.Id sets differ:" + " [] only in A; [" + ps2.getId() + "] only in B",
-        "PatchSetApproval.Key sets differ:" + " [] only in A; [" + a2.getKey() + "] only in B");
+        "ChangeMessages differ for Change.Id " + c.getId() + "\nOnly in B:\n  " + cm2,
+        "PatchSet.Id sets differ: [] only in A; [" + ps2.getId() + "] only in B",
+        "PatchSetApproval.Key sets differ: [] only in A; [" + a2.getKey() + "] only in B");
   }
 
   @Test
@@ -1265,7 +1262,7 @@
         new ChangeBundle(
             c, messages(), patchSets(ps2), approvals(), comments(), reviewers(), REVIEW_DB);
     assertDiffs(
-        b1, b2, "description differs for PatchSet.Id " + ps1.getId() + ":" + " { abc } != {abc}");
+        b1, b2, "description differs for PatchSet.Id " + ps1.getId() + ": { abc } != {abc}");
 
     // Whitespace in ReviewDb description is ignored.
     b1 =
@@ -1287,7 +1284,7 @@
         new ChangeBundle(
             c, messages(), patchSets(ps3), approvals(), comments(), reviewers(), NOTE_DB);
     assertDiffs(
-        b1, b2, "description differs for PatchSet.Id " + ps1.getId() + ":" + " { abc } != {cba}");
+        b1, b2, "description differs for PatchSet.Id " + ps1.getId() + ": { abc } != {cba}");
   }
 
   @Test
@@ -1617,7 +1614,7 @@
         new ChangeBundle(c, messages(), latest(c), approvals(), comments(), r2, REVIEW_DB);
     assertNoDiffs(b1, b1);
     assertNoDiffs(b2, b2);
-    assertDiffs(b1, b2, "reviewer sets differ:" + " [1] only in A;" + " [2] only in B");
+    assertDiffs(b1, b2, "reviewer sets differ: [1] only in A; [2] only in B");
   }
 
   @Test
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index abc87d4..39c4c08 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -57,7 +57,7 @@
             + "Subject: This is a test change\n");
     assertParseFails(
         writeCommit(
-            "Update change\n" + "\n" + "Patch-set: 1\n",
+            "Update change\n\nPatch-set: 1\n",
             new PersonIdent(
                 "Change Owner",
                 "owner@example.com",
@@ -65,12 +65,12 @@
                 serverIdent.getTimeZone())));
     assertParseFails(
         writeCommit(
-            "Update change\n" + "\n" + "Patch-set: 1\n",
+            "Update change\n\nPatch-set: 1\n",
             new PersonIdent(
                 "Change Owner", "x@gerrit", serverIdent.getWhen(), serverIdent.getTimeZone())));
     assertParseFails(
         writeCommit(
-            "Update change\n" + "\n" + "Patch-set: 1\n",
+            "Update change\n\nPatch-set: 1\n",
             new PersonIdent(
                 "Change\n\u1234<Owner>",
                 "\n\nx<@>\u0002gerrit",
@@ -96,9 +96,8 @@
             + "Patch-set: 1\n"
             + "Status: new\n"
             + "Subject: This is a test change\n");
-    assertParseFails("Update change\n" + "\n" + "Patch-set: 1\n" + "Status: OOPS\n");
-    assertParseFails(
-        "Update change\n" + "\n" + "Patch-set: 1\n" + "Status: NEW\n" + "Status: NEW\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nStatus: OOPS\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nStatus: NEW\nStatus: NEW\n");
   }
 
   @Test
@@ -110,8 +109,8 @@
             + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
             + "Patch-set: 1\n"
             + "Subject: This is a test change\n");
-    assertParseFails("Update change\n" + "\n");
-    assertParseFails("Update change\n" + "\n" + "Patch-set: 1\n" + "Patch-set: 1\n");
+    assertParseFails("Update change\n\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nPatch-set: 1\n");
     assertParseSucceeds(
         "Update change\n"
             + "\n"
@@ -119,7 +118,7 @@
             + "Change-id: I577fb248e474018276351785930358ec0450e9f7\n"
             + "Patch-set: 1\n"
             + "Subject: This is a test change\n");
-    assertParseFails("Update change\n" + "\n" + "Patch-set: x\n");
+    assertParseFails("Update change\n\nPatch-set: x\n");
   }
 
   @Test
@@ -144,14 +143,12 @@
             + "Label: -Label1\n"
             + "Label: -Label1 Other Account <2@gerrit>\n"
             + "Subject: This is a test change\n");
-    assertParseFails("Update change\n" + "\n" + "Patch-set: 1\n" + "Label: Label1=X\n");
-    assertParseFails("Update change\n" + "\n" + "Patch-set: 1\n" + "Label: Label1 = 1\n");
-    assertParseFails("Update change\n" + "\n" + "Patch-set: 1\n" + "Label: X+Y\n");
-    assertParseFails(
-        "Update change\n" + "\n" + "Patch-set: 1\n" + "Label: Label1 Other Account <2@gerrit>\n");
-    assertParseFails("Update change\n" + "\n" + "Patch-set: 1\n" + "Label: -Label!1\n");
-    assertParseFails(
-        "Update change\n" + "\n" + "Patch-set: 1\n" + "Label: -Label!1 Other Account <2@gerrit>\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nLabel: Label1=X\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nLabel: Label1 = 1\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nLabel: X+Y\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nLabel: Label1 Other Account <2@gerrit>\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nLabel: -Label!1\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nLabel: -Label!1 Other Account <2@gerrit>\n");
   }
 
   @Test
@@ -169,8 +166,8 @@
             + "Submitted-with: NOT_READY\n"
             + "Submitted-with: OK: Verified: Change Owner <1@gerrit>\n"
             + "Submitted-with: NEED: Alternative-Code-Review\n");
-    assertParseFails("Update change\n" + "\n" + "Patch-set: 1\n" + "Submitted-with: OOPS\n");
-    assertParseFails("Update change\n" + "\n" + "Patch-set: 1\n" + "Submitted-with: NEED: X+Y\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nSubmitted-with: OOPS\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nSubmitted-with: NEED: X+Y\n");
     assertParseFails(
         "Update change\n"
             + "\n"
@@ -212,7 +209,7 @@
             + "Reviewer: Change Owner <1@gerrit>\n"
             + "CC: Other Account <2@gerrit>\n"
             + "Subject: This is a test change\n");
-    assertParseFails("Update change\n" + "\n" + "Patch-set: 1\n" + "Reviewer: 1@gerrit\n");
+    assertParseFails("Update change\n\nPatch-set: 1\nReviewer: 1@gerrit\n");
   }
 
   @Test
@@ -233,8 +230,7 @@
             + "Patch-set: 1\n"
             + "Topic:\n"
             + "Subject: This is a test change\n");
-    assertParseFails(
-        "Update change\n" + "\n" + "Patch-set: 1\n" + "Topic: Some Topic\n" + "Topic: Other Topic");
+    assertParseFails("Update change\n\nPatch-set: 1\nTopic: Some Topic\nTopic: Other Topic");
   }
 
   @Test
@@ -453,10 +449,10 @@
 
   @Test
   public void currentPatchSet() throws Exception {
-    assertParseSucceeds("Update change\n" + "\n" + "Patch-set: 1\n" + "Current: true");
-    assertParseSucceeds("Update change\n" + "\n" + "Patch-set: 1\n" + "Current: tRUe");
-    assertParseFails("Update change\n" + "\n" + "Patch-set: 1\n" + "Current: false");
-    assertParseFails("Update change\n" + "\n" + "Patch-set: 1\n" + "Current: blah");
+    assertParseSucceeds("Update change\n\nPatch-set: 1\nCurrent: true");
+    assertParseSucceeds("Update change\n\nPatch-set: 1\nCurrent: tRUe");
+    assertParseFails("Update change\n\nPatch-set: 1\nCurrent: false");
+    assertParseFails("Update change\n\nPatch-set: 1\nCurrent: blah");
   }
 
   private RevCommit writeCommit(String body) throws Exception {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
index a4990c1..9d6cb60 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.notedb;
 
 import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assert_;
 import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
 import static com.google.gerrit.server.notedb.ReviewerStateInternal.CC;
@@ -51,6 +52,7 @@
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gerrit.server.util.RequestId;
 import com.google.gerrit.testutil.TestChanges;
+import com.google.gerrit.testutil.TestTimeUtil;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import java.sql.Timestamp;
@@ -58,6 +60,7 @@
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -1376,7 +1379,7 @@
   public void changeMessageWithTrailingDoubleNewline() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setChangeMessage("Testing trailing double newline\n" + "\n");
+    update.setChangeMessage("Testing trailing double newline\n\n");
     update.commit();
     PatchSet.Id ps1 = c.currentPatchSetId();
 
@@ -1385,7 +1388,7 @@
     assertThat(changeMessages).hasSize(1);
 
     ChangeMessage cm1 = Iterables.getOnlyElement(changeMessages.get(ps1));
-    assertThat(cm1.getMessage()).isEqualTo("Testing trailing double newline\n" + "\n");
+    assertThat(cm1.getMessage()).isEqualTo("Testing trailing double newline\n\n");
     assertThat(cm1.getAuthor()).isEqualTo(changeOwner.getAccount().getId());
   }
 
@@ -1393,8 +1396,7 @@
   public void changeMessageWithMultipleParagraphs() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setChangeMessage(
-        "Testing paragraph 1\n" + "\n" + "Testing paragraph 2\n" + "\n" + "Testing paragraph 3");
+    update.setChangeMessage("Testing paragraph 1\n\nTesting paragraph 2\n\nTesting paragraph 3");
     update.commit();
     PatchSet.Id ps1 = c.currentPatchSetId();
 
@@ -3193,6 +3195,76 @@
     assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(2);
   }
 
+  @Test
+  public void readOnlyUntilExpires() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    Timestamp until = new Timestamp(TimeUtil.nowMs() + 10000);
+    update.setReadOnlyUntil(until);
+    update.commit();
+
+    update = newUpdate(c, changeOwner);
+    update.setTopic("failing-topic");
+    try {
+      update.commit();
+      assert_().fail("expected OrmException");
+    } catch (OrmException e) {
+      assertThat(e.getMessage()).contains("read-only until");
+    }
+
+    ChangeNotes notes = newNotes(c);
+    assertThat(notes.getChange().getTopic()).isNotEqualTo("failing-topic");
+    assertThat(notes.getReadOnlyUntil()).isEqualTo(until);
+
+    TestTimeUtil.incrementClock(30, TimeUnit.SECONDS);
+    update = newUpdate(c, changeOwner);
+    update.setTopic("succeeding-topic");
+    update.commit();
+
+    // Write succeeded; lease still exists, even though it's expired.
+    notes = newNotes(c);
+    assertThat(notes.getChange().getTopic()).isEqualTo("succeeding-topic");
+    assertThat(notes.getReadOnlyUntil()).isEqualTo(until);
+
+    // New lease takes precedence.
+    update = newUpdate(c, changeOwner);
+    until = new Timestamp(TimeUtil.nowMs() + 10000);
+    update.setReadOnlyUntil(until);
+    update.commit();
+    assertThat(newNotes(c).getReadOnlyUntil()).isEqualTo(until);
+  }
+
+  @Test
+  public void readOnlyUntilCleared() throws Exception {
+    Change c = newChange();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    Timestamp until = new Timestamp(TimeUtil.nowMs() + TimeUnit.DAYS.toMillis(30));
+    update.setReadOnlyUntil(until);
+    update.commit();
+
+    update = newUpdate(c, changeOwner);
+    update.setTopic("failing-topic");
+    try {
+      update.commit();
+      assert_().fail("expected OrmException");
+    } catch (OrmException e) {
+      assertThat(e.getMessage()).contains("read-only until");
+    }
+
+    // Sentinel timestamp of 0 can be written to clear lease.
+    update = newUpdate(c, changeOwner);
+    update.setReadOnlyUntil(new Timestamp(0));
+    update.commit();
+
+    update = newUpdate(c, changeOwner);
+    update.setTopic("succeeding-topic");
+    update.commit();
+
+    ChangeNotes notes = newNotes(c);
+    assertThat(notes.getChange().getTopic()).isEqualTo("succeeding-topic");
+    assertThat(notes.getReadOnlyUntil()).isEqualTo(new Timestamp(0));
+  }
+
   private boolean testJson() {
     return noteUtil.getWriteJson();
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index 2e58382..25b5168 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -84,7 +84,7 @@
   public void changeMessageCommitFormatSimple() throws Exception {
     Change c = TestChanges.newChange(project, changeOwner.getAccountId(), 1);
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setChangeMessage("Just a little code change.\n" + "How about a new line");
+    update.setChangeMessage("Just a little code change.\nHow about a new line");
     update.commit();
     assertThat(update.getRefName()).isEqualTo("refs/changes/01/1/meta");
 
@@ -141,8 +141,7 @@
     update.commit();
 
     assertBodyEquals(
-        "Update patch set 1\n" + "\n" + "Patch-set: 1\n" + "Label: -Code-Review\n",
-        update.getResult());
+        "Update patch set 1\n\nPatch-set: 1\nLabel: -Code-Review\n", update.getResult());
   }
 
   @Test
@@ -207,9 +206,7 @@
     update.commit();
 
     RevCommit commit = parseCommit(update.getResult());
-    assertBodyEquals(
-        "Update patch set 1\n" + "\n" + "Comment on the change.\n" + "\n" + "Patch-set: 1\n",
-        commit);
+    assertBodyEquals("Update patch set 1\n\nComment on the change.\n\nPatch-set: 1\n", commit);
 
     PersonIdent author = commit.getAuthorIdent();
     assertThat(author.getName()).isEqualTo("Anonymous Coward (3)");
@@ -247,7 +244,7 @@
     update.commit();
 
     assertBodyEquals(
-        "Update patch set 1\n" + "\n" + "Patch-set: 1\n" + "Reviewer: Change Owner <1@gerrit>\n",
+        "Update patch set 1\n\nPatch-set: 1\nReviewer: Change Owner <1@gerrit>\n",
         update.getResult());
   }
 
@@ -255,7 +252,7 @@
   public void changeMessageWithTrailingDoubleNewline() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setChangeMessage("Testing trailing double newline\n" + "\n");
+    update.setChangeMessage("Testing trailing double newline\n\n");
     update.commit();
 
     assertBodyEquals(
@@ -273,8 +270,7 @@
   public void changeMessageWithMultipleParagraphs() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
-    update.setChangeMessage(
-        "Testing paragraph 1\n" + "\n" + "Testing paragraph 2\n" + "\n" + "Testing paragraph 3");
+    update.setChangeMessage("Testing paragraph 1\n\nTesting paragraph 2\n\nTesting paragraph 3");
     update.commit();
 
     assertBodyEquals(
@@ -383,8 +379,7 @@
     update.setCurrentPatchSet();
     update.commit();
 
-    assertBodyEquals(
-        "Update patch set 1\n" + "\n" + "Patch-set: 1\n" + "Current: true\n", update.getResult());
+    assertBodyEquals("Update patch set 1\n\nPatch-set: 1\nCurrent: true\n", update.getResult());
   }
 
   private RevCommit parseCommit(ObjectId id) throws Exception {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
index cc3519d..7eda3cc 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
@@ -21,7 +21,6 @@
 import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.LabelValue;
-import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -49,20 +48,13 @@
 
   @Inject private InMemoryDatabase db;
 
-  private LifecycleManager lifecycle;
-
   @Before
   public void setUp() throws Exception {
-    lifecycle = new LifecycleManager();
     new InMemoryModule().inject(this);
-    lifecycle.start();
   }
 
   @After
   public void tearDown() throws Exception {
-    if (lifecycle != null) {
-      lifecycle.stop();
-    }
     InMemoryDatabase.drop(db);
   }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
index ec0dad5..f53a59b 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
@@ -245,7 +245,7 @@
             "\n"
             + //
             "Change-Id: Id0b4f42d3d6fc1569595c9b97cb665e738486f5d\n", //
-        call("a\n" + "\n" + "b\n"));
+        call("a\n\nb\n"));
 
     assertEquals(
         "a\n"
@@ -257,7 +257,7 @@
             "\n"
             + //
             "Change-Id: I7d237b20058a0f46cc3f5fabc4a0476877289d75\n", //
-        call("a\n" + "\n" + "b\nc\nd\ne\n"));
+        call("a\n\nb\nc\nd\ne\n"));
 
     assertEquals(
         "a\n"
@@ -273,7 +273,7 @@
             "\n"
             + //
             "Change-Id: I382e662f47bf164d6878b7fe61637873ab7fa4e8\n", //
-        call("a\n" + "\n" + "b\nc\nd\ne\n" + "\n" + "f\ng\nh\n"));
+        call("a\n\nb\nc\nd\ne\n\nf\ng\nh\n"));
   }
 
   @Test
@@ -286,7 +286,7 @@
             "Change-Id: I7fc3876fee63c766a2063df97fbe04a2dddd8d7c\n"
             + //
             SOB1, //
-        call("a\n" + "\n" + SOB1));
+        call("a\n\n" + SOB1));
 
     assertEquals(
         "a\n"
@@ -298,7 +298,7 @@
             SOB1
             + //
             SOB2, //
-        call("a\n" + "\n" + SOB1 + SOB2));
+        call("a\n\n" + SOB1 + SOB2));
   }
 
   @Test
@@ -319,7 +319,7 @@
             "Change-Id: I382e662f47bf164d6878b7fe61637873ab7fa4e8\n"
             + //
             SOB1, //
-        call("a\n" + "\n" + "b\nc\nd\ne\n" + "\n" + "f\ng\nh\n" + "\n" + SOB1));
+        call("a\n\nb\nc\nd\ne\n\nf\ng\nh\n\n" + SOB1));
 
     assertEquals(
         "a\n"
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
index 9a41a9c..27a0172 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
@@ -61,7 +61,7 @@
   private static synchronized DataSource newDataSource() throws SQLException {
     final Properties p = new Properties();
     p.setProperty("driver", org.h2.Driver.class.getName());
-    p.setProperty("url", "jdbc:h2:mem:" + "Test_" + (++dbCnt));
+    p.setProperty("url", "jdbc:h2:mem:Test_" + (++dbCnt));
     return new SimpleDataSource(p);
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index 1c85518..8b323dc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -151,7 +151,7 @@
     name = "--branch",
     aliases = {"-b"},
     metaVar = "BRANCH",
-    usage = "initial branch name\n" + "(default: master)"
+    usage = "initial branch name\n(default: master)"
   )
   private List<String> branch;
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index a405a8f..ca69b54 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -141,7 +141,7 @@
 
   @Option(
     name = "--strict-labels",
-    usage = "Strictly check if the labels " + "specified can be applied to the given patch set(s)"
+    usage = "Strictly check if the labels specified can be applied to the given patch set(s)"
   )
   private boolean strictLabels;
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index ce58921..7baaeb6 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -155,7 +155,7 @@
       throw die("--active and --inactive options are mutually exclusive.");
     }
     if (clearHttpPassword && !Strings.isNullOrEmpty(httpPassword)) {
-      throw die("--http-password and --clear-http-password options are " + "mutually exclusive.");
+      throw die("--http-password and --clear-http-password options are mutually exclusive.");
     }
     if (addSshKeys.contains("-") && deleteSshKeys.contains("-")) {
       throw die("Only one option may use the stdin");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index 53f76e9..0311b88 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -54,7 +54,7 @@
   @Option(
     name = "--submit-type",
     aliases = {"-t"},
-    usage = "project submit type\n" + "(default: MERGE_IF_NECESSARY)"
+    usage = "project submit type\n(default: MERGE_IF_NECESSARY)"
   )
   private SubmitType submitType;
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
index 2b8f9aa..13db697 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -90,7 +90,7 @@
             "%-8s %-12s %-12s %-4s %s\n", //
             "Task", "State", "StartTime", "", "Command"));
     stdout.print(
-        "----------------------------------------------" + "--------------------------------\n");
+        "------------------------------------------------------------------------------\n");
 
     List<TaskInfo> tasks;
     try {
@@ -163,7 +163,7 @@
       }
     }
     stdout.print(
-        "----------------------------------------------" + "--------------------------------\n");
+        "------------------------------------------------------------------------------\n");
     stdout.print("  " + tasks.size() + " tasks");
     if (threadPoolSize > 0) {
       stdout.print(", " + threadPoolSize + " worker threads");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
index 1ce3dc9..4b8771a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/UploadArchive.java
@@ -101,11 +101,7 @@
     )
     private boolean level9;
 
-    @Argument(
-      index = 0,
-      required = true,
-      usage = "The tree or commit to " + "produce an archive for."
-    )
+    @Argument(index = 0, required = true, usage = "The tree or commit to produce an archive for.")
     private String treeIsh = "master";
 
     @Argument(
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java
index af52e3d3..0ade137 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/plugin/LfsPluginAuthCommand.java
@@ -65,7 +65,7 @@
     LfsSshPluginAuth pluginAuth = auth.get();
     if (pluginAuth == null) {
       throw new Failure(
-          1, "Server configuration error:" + " LFS auth over SSH is not properly configured.");
+          1, "Server configuration error: LFS auth over SSH is not properly configured.");
     }
 
     stdout.print(pluginAuth.authenticate(user.get(), args));
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/SitePathFromSystemConfigProvider.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/SitePathFromSystemConfigProvider.java
index dee5621..0535f26 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/SitePathFromSystemConfigProvider.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/SitePathFromSystemConfigProvider.java
@@ -49,7 +49,7 @@
           throw new OrmException("system_config table is empty");
         default:
           throw new OrmException(
-              "system_config must have exactly 1 row;" + " found " + all.size() + " rows instead");
+              "system_config must have exactly 1 row; found " + all.size() + " rows instead");
       }
     }
   }
diff --git a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
index 8ec1055..82c6afb 100644
--- a/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
+++ b/polygerrit-ui/app/elements/core/gr-keyboard-shortcuts-dialog/gr-keyboard-shortcuts-dialog.html
@@ -96,7 +96,7 @@
           </tr>
           <tr>
             <td><span class="key">u</span></td>
-            <td>Up to change list</td>
+            <td>Up to dashboard</td>
           </tr>
         </tbody>
         <!-- Diff View -->
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index 74dc9e7..dc66bb6 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -61,8 +61,9 @@
         justify-content: flex-end;
       }
       gr-search-bar {
+        flex-grow: 1;
         margin-left: .5em;
-        width: 500px;
+        max-width: 500px;
       }
       .accountContainer:not(.loggedIn):not(.loggedOut) .loginButton,
       .accountContainer:not(.loggedIn):not(.loggedOut) gr-account-dropdown,
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index ebeb9af..c9abac2 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -14,6 +14,32 @@
 (function() {
   'use strict';
 
+  var ADMIN_LINKS = [
+    {
+      url: '/admin/groups',
+      name: 'Groups',
+    },
+    {
+      url: '/admin/create-group',
+      name: 'Create Group',
+      capability: 'createGroup'
+    },
+    {
+      url: '/admin/projects',
+      name: 'Projects',
+    },
+    {
+      url: '/admin/create-project',
+      name: 'Create Project',
+      capability: 'createProject',
+    },
+    {
+      url: '/admin/plugins',
+      name: 'Plugins',
+      capability: 'viewPlugins',
+    },
+  ];
+
   var DEFAULT_LINKS = [{
     title: 'Changes',
     links: [
@@ -46,6 +72,10 @@
       },
 
       _account: Object,
+      _adminLinks: {
+        type: Array,
+        value: function() { return []; },
+      },
       _defaultLinks: {
         type: Array,
         value: function() {
@@ -54,7 +84,7 @@
       },
       _links: {
         type: Array,
-        computed: '_computeLinks(_defaultLinks, _userLinks)',
+        computed: '_computeLinks(_defaultLinks, _userLinks, _adminLinks)',
       },
       _loginURL: {
         type: String,
@@ -94,7 +124,7 @@
       return '//' + window.location.host + path;
     },
 
-    _computeLinks: function(defaultLinks, userLinks) {
+    _computeLinks: function(defaultLinks, userLinks, adminLinks) {
       var links = defaultLinks.slice();
       if (userLinks && userLinks.length > 0) {
         links.push({
@@ -102,6 +132,12 @@
           links: userLinks,
         });
       }
+      if (adminLinks && adminLinks.length > 0) {
+        links.push({
+          title: 'Admin',
+          links: adminLinks,
+        });
+      }
       return links;
     },
 
@@ -120,6 +156,18 @@
         this._userLinks =
             prefs.my.map(this._stripHashPrefix).filter(this._isSupportedLink);
       }.bind(this));
+      this._loadAccountCapabilities();
+    },
+
+    _loadAccountCapabilities: function() {
+      var params = ['createProject', 'createGroup', 'viewPlugins'];
+      return this.$.restAPI.getAccountCapabilities(params)
+          .then(function(capabilities) {
+        this._adminLinks = ADMIN_LINKS.filter(function(link) {
+          return !link.capability ||
+              capabilities.hasOwnProperty(link.capability);
+        });
+      }.bind(this));
     },
 
     _stripHashPrefix: function(linkObj) {
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
index aef338b..75723b9 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.html
@@ -33,8 +33,10 @@
 <script>
   suite('gr-main-header tests', function() {
     var element;
+    var sandbox;
 
     setup(function() {
+      sandbox = sinon.sandbox.create();
       stub('gr-rest-api-interface', {
         getConfig: function() { return Promise.resolve({}); },
       });
@@ -44,6 +46,10 @@
       element = fixture('basic');
     });
 
+    teardown(function() {
+      sandbox.restore();
+    });
+
     test('strip hash prefix', function() {
       assert.deepEqual([
         {url: '#/q/owner:self+is:draft'},
@@ -69,6 +75,30 @@
       ]);
     });
 
+    test('_loadAccountCapabilities admin', function(done) {
+      sandbox.stub(element.$.restAPI, 'getAccountCapabilities', function() {
+        return Promise.resolve({
+          createGroup: true,
+          createProject: true,
+          viewPlugins: true,
+        });
+      });
+      element._loadAccountCapabilities().then(function() {
+        assert.equal(element._adminLinks.length, 5);
+        done();
+      });
+    });
+
+    test('_loadAccountCapabilities non admin', function(done) {
+      sandbox.stub(element.$.restAPI, 'getAccountCapabilities', function() {
+        return Promise.resolve({});
+      });
+      element._loadAccountCapabilities().then(function() {
+        assert.equal(element._adminLinks.length, 2);
+        done();
+      });
+    });
+
     test('user links', function() {
       var defaultLinks = [{
         title: 'Faves',
@@ -81,11 +111,21 @@
         name: 'Facebook',
         url: 'https://facebook.com',
       }];
-      assert.deepEqual(element._computeLinks(defaultLinks, []), defaultLinks);
-      assert.deepEqual(element._computeLinks(defaultLinks, userLinks),
+      var adminLinks = [{
+        url: '/admin/groups',
+        name: 'Groups',
+      }];
+
+      assert.deepEqual(
+          element._computeLinks(defaultLinks, [], []), defaultLinks);
+      assert.deepEqual(
+          element._computeLinks(defaultLinks, userLinks, adminLinks),
           defaultLinks.concat({
             title: 'Your',
             links: userLinks,
+          }, {
+            title: 'Admin',
+            links: adminLinks,
           }));
     });
   });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
index c088b1a..81c2752 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread.js
@@ -225,7 +225,7 @@
 
     _handleCommentAck: function(e) {
       var comment = this._lastComment;
-      this._createReplyComment(comment, 'Ack', false, comment.unresolved);
+      this._createReplyComment(comment, 'Ack', false, false);
     },
 
     _handleCommentDone: function(e) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
index e0ae9ff..da61e41 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-comment-thread/gr-diff-comment-thread_test.html
@@ -278,6 +278,7 @@
         assert.equal(drafts.length, 1);
         assert.equal(drafts[0].message, 'Ack');
         assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
+        assert.equal(drafts[0].unresolved, false);
         done();
       });
     });
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
index 10f04c6..c354882a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection.js
@@ -24,6 +24,8 @@
     RIGHT: 'selected-right',
   };
 
+  var getNewCache = function() { return {left: null, right: null}; };
+
   Polymer({
     is: 'gr-diff-selection',
 
@@ -32,10 +34,14 @@
       _cachedDiffBuilder: Object,
       _linesCache: {
         type: Object,
-        value: function() { return {left: null, right: null}; },
+        value: getNewCache(),
       },
     },
 
+    observers: [
+      '_diffChanged(diff)',
+    ],
+
     listeners: {
       'copy': '_handleCopy',
       'down': '_handleDown',
@@ -53,6 +59,10 @@
       return this._cachedDiffBuilder;
     },
 
+    _diffChanged: function() {
+      this._linesCache = getNewCache();
+    },
+
     _handleDown: function(e) {
       var lineEl = this.diffBuilder.getLineElByChild(e.target);
       if (!lineEl) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
index d517a80..86edf6b 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-selection/gr-diff-selection_test.html
@@ -325,5 +325,12 @@
             'is is a co');
       });
     });
+
+    test('cache is reset when diff changes', function() {
+      element._linesCache = {left: 'test', right: 'test'};
+      element.diff = {};
+      flushAsynchronousOperations();
+      assert.deepEqual(element._linesCache, {left: null, right: null});
+    });
   });
 </script>
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
index 61c3a44..0ce3aba 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.js
@@ -64,7 +64,6 @@
     'gr-diff gr-syntax gr-syntax-string': true,
     'gr-diff gr-syntax gr-syntax-selector-id': true,
     'gr-diff gr-syntax gr-syntax-title': true,
-    'gr-diff gr-syntax gr-syntax-params': true,
     'gr-diff gr-syntax gr-syntax-comment': true,
     'gr-diff gr-syntax gr-syntax-meta': true,
     'gr-diff gr-syntax gr-syntax-meta-keyword': true,
diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
index 633e24a..fabc347 100644
--- a/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
+++ b/polygerrit-ui/app/elements/diff/gr-syntax-themes/gr-theme-default.html
@@ -60,7 +60,6 @@
       .gr-syntax-meta-keyword {
         color: #219;
       }
-      .gr-syntax-params,
       .gr-syntax-type {
         color: #00f;
       }
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index 5c2ad5c..b9b1f53 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -317,6 +317,17 @@
       return this._fetchSharedCacheURL('/accounts/self/groups');
     },
 
+    getAccountCapabilities: function(opt_params) {
+      var queryString = '';
+      if (opt_params) {
+        queryString = '?q=' + opt_params
+            .map(function(param) { return encodeURIComponent(param); })
+            .join('&q=');
+      }
+      return this._fetchSharedCacheURL('/accounts/self/capabilities' +
+          queryString);
+    },
+
     getLoggedIn: function() {
       return this.getAccount().then(function(account) {
         return account != null;