Merge "Support jumping to next/previous file with comments via shortcut"
diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt
index cccfe5c..0d0717e 100644
--- a/Documentation/config-validation.txt
+++ b/Documentation/config-validation.txt
@@ -45,6 +45,18 @@
 If the commit fails the validation, the plugin can throw an exception
 which will cause the merge to fail.
 
+[[on-submit-validation]]
+== On submit validation
+
+
+Plugins implementing the `OnSubmitValidationListener` interface can
+perform additional validation checks against ref operations resuling
+from execution of submit operation before they are applied to any git
+repositories (there could be more than one in case of topic submits).
+
+Plugin can throw an exception which will cause submit operation to be
+aborted.
+
 [[pre-upload-validation]]
 == Pre-upload validation
 
diff --git a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
index 4b57132..39d06f3 100644
--- a/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
+++ b/gerrit-acceptance-framework/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -25,6 +25,8 @@
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.AsyncReceiveCommits;
 import com.google.gerrit.server.ssh.NoSshModule;
+import com.google.gerrit.server.util.ManualRequestContext;
+import com.google.gerrit.server.util.OneOffRequestContext;
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.gerrit.server.util.SystemLog;
 import com.google.gerrit.testutil.FakeEmailSender;
@@ -62,6 +64,7 @@
     static Description forTestClass(org.junit.runner.Description testDesc,
         String configName) {
       return new AutoValue_GerritServer_Description(
+          testDesc,
           configName,
           true, // @UseLocalDisk is only valid on methods.
           !has(NoHttpd.class, testDesc.getTestClass()),
@@ -75,6 +78,7 @@
     static Description forTestMethod(org.junit.runner.Description testDesc,
         String configName) {
       return new AutoValue_GerritServer_Description(
+          testDesc,
           configName,
           testDesc.getAnnotation(UseLocalDisk.class) == null,
           testDesc.getAnnotation(NoHttpd.class) == null
@@ -97,6 +101,7 @@
       return false;
     }
 
+    abstract org.junit.runner.Description testDescription();
     @Nullable abstract String configName();
     abstract boolean memory();
     abstract boolean httpd();
@@ -297,10 +302,7 @@
 
   void stop() throws Exception {
     try {
-      if (NoteDbMode.get().equals(NoteDbMode.CHECK)) {
-        testInjector.getInstance(NoteDbChecker.class)
-            .rebuildAndCheckAllChanges();
-      }
+      checkNoteDbState();
     } finally {
       daemon.getLifecycleManager().stop();
       if (daemonService != null) {
@@ -312,6 +314,23 @@
     }
   }
 
+  private void checkNoteDbState() throws Exception {
+    NoteDbMode mode = NoteDbMode.get();
+    if (mode != NoteDbMode.CHECK && mode != NoteDbMode.PRIMARY) {
+      return;
+    }
+    NoteDbChecker checker = testInjector.getInstance(NoteDbChecker.class);
+    OneOffRequestContext oneOffRequestContext =
+        testInjector.getInstance(OneOffRequestContext.class);
+    try (ManualRequestContext ctx = oneOffRequestContext.open()) {
+      if (mode == NoteDbMode.CHECK) {
+        checker.rebuildAndCheckAllChanges();
+      } else if (mode == NoteDbMode.PRIMARY) {
+        checker.assertNoReviewDbChanges(desc.testDescription());
+      }
+    }
+  }
+
   @Override
   public String toString() {
     return MoreObjects.toStringHelper(this).addValue(desc).toString();
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 02d74bf..1e12638 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
@@ -99,6 +99,7 @@
 import com.google.gerrit.server.git.ChangeMessageModifier;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.group.SystemGroupBackend;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.Util;
 import com.google.gerrit.testutil.FakeEmailSender.Message;
@@ -1011,6 +1012,8 @@
   public void addReviewerWithNoteDbWhenDummyApprovalInReviewDbExists()
       throws Exception {
     assume().that(notesMigration.enabled()).isTrue();
+    assume().that(notesMigration.changePrimaryStorage())
+        .isEqualTo(PrimaryStorage.REVIEW_DB);
 
     PushOneCommit.Result r = createChange();
 
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
index 521ccc4..a7002fc 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/RefAdvertisementIT.java
@@ -43,6 +43,7 @@
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.Util;
 import com.google.gerrit.server.query.change.ChangeData;
@@ -447,8 +448,6 @@
 
   @Test
   public void receivePackOmitsMissingObject() throws Exception {
-    // Use the tactic from ConsistencyCheckerIT to insert a new patch set with a
-    // missing object.
     String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
     try (Repository repo = repoManager.openRepository(project)) {
       TestRepository<?> tr = new TestRepository<>(repo);
@@ -457,9 +456,11 @@
       PatchSet.Id psId = new PatchSet.Id(c3.getId(), 2);
       c.setCurrentPatchSet(psId, subject, c.getOriginalSubject());
 
-      PatchSet ps = TestChanges.newPatchSet(psId, rev, admin.getId());
-      db.patchSets().insert(Collections.singleton(ps));
-      db.changes().update(Collections.singleton(c));
+      if (notesMigration.changePrimaryStorage() == PrimaryStorage.REVIEW_DB) {
+        PatchSet ps = TestChanges.newPatchSet(psId, rev, admin.getId());
+        db.patchSets().insert(Collections.singleton(ps));
+        db.changes().update(Collections.singleton(c));
+      }
 
       if (notesMigration.commitChangeWrites()) {
         PersonIdent committer = serverIdent.get();
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index 45da3d8..2a39ffd 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -29,6 +29,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.NoHttpd;
 import com.google.gerrit.acceptance.PushOneCommit;
@@ -45,6 +46,8 @@
 import com.google.gerrit.extensions.client.SubmitType;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.LabelInfo;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BinaryResult;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
@@ -56,6 +59,7 @@
 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.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.RevisionResource;
@@ -63,8 +67,10 @@
 import com.google.gerrit.server.git.BatchUpdate;
 import com.google.gerrit.server.git.BatchUpdate.ChangeContext;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.project.Util;
+import com.google.gerrit.server.validators.ValidationException;
 import com.google.gerrit.testutil.ConfigSuite;
 import com.google.gerrit.testutil.TestTimeUtil;
 import com.google.gwtorm.server.OrmException;
@@ -86,9 +92,13 @@
 import org.junit.Test;
 
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.stream.Collectors;
 
 @NoHttpd
@@ -110,6 +120,10 @@
   @Inject
   private BatchUpdate.Factory updateFactory;
 
+  @Inject
+  private DynamicSet<OnSubmitValidationListener> onSubmitValidationListeners;
+  private RegistrationHandle onSubmitValidatorHandle;
+
   private String systemTimeZone;
 
   @Before
@@ -129,6 +143,13 @@
     db.close();
   }
 
+  @After
+  public void removeOnSubmitValidator(){
+    if (onSubmitValidatorHandle != null){
+      onSubmitValidatorHandle.remove();
+    }
+  }
+
   protected abstract SubmitType getSubmitType();
 
   @Test
@@ -647,6 +668,94 @@
     assertThat(getRemoteHead()).isEqualTo(headAfterSubmit);
   }
 
+  @Test
+  public void submitWithValidation() throws Exception {
+    AtomicBoolean called = new AtomicBoolean(false);
+    this.addOnSubmitValidationListener(new OnSubmitValidationListener() {
+      @Override
+      public void preBranchUpdate(Arguments args) throws ValidationException {
+        called.set(true);
+        HashSet<String> refs = Sets.newHashSet(args.getCommands().keySet());
+        assertThat(refs).contains("refs/heads/master");
+        refs.remove("refs/heads/master");
+        if (!refs.isEmpty()){
+          // Some submit strategies need to insert new patchset.
+          assertThat(refs).hasSize(1);
+          assertThat(refs.iterator().next()).startsWith(RefNames.REFS_CHANGES);
+        }
+      }
+    });
+
+    PushOneCommit.Result change = createChange();
+    approve(change.getChangeId());
+    submit(change.getChangeId());
+    assertThat(called.get()).isTrue();
+  }
+
+  @Test
+  public void submitWithValidationMultiRepo() throws Exception {
+    assume().that(isSubmitWholeTopicEnabled()).isTrue();
+    String topic = "test-topic";
+
+    // Create test projects
+    TestRepository<?> repoA =
+        createProjectWithPush("project-a", null, getSubmitType());
+    TestRepository<?> repoB =
+        createProjectWithPush("project-b", null, getSubmitType());
+
+    // Create changes on project-a
+    PushOneCommit.Result change1 =
+        createChange(repoA, "master", "Change 1", "a.txt", "content", topic);
+    PushOneCommit.Result change2 =
+        createChange(repoA, "master", "Change 2", "b.txt", "content", topic);
+
+    // Create changes on project-b
+    PushOneCommit.Result change3 =
+        createChange(repoB, "master", "Change 3", "a.txt", "content", topic);
+    PushOneCommit.Result change4 =
+        createChange(repoB, "master", "Change 4", "b.txt", "content", topic);
+
+    List<PushOneCommit.Result> changes =
+        Lists.newArrayList(change1, change2, change3, change4);
+    for (PushOneCommit.Result change : changes) {
+      approve(change.getChangeId());
+    }
+
+    // Construct validator which will throw on a second call.
+    // Since there are 2 repos, first submit attempt will fail, the second will
+    // succeed.
+    List<String> projectsCalled = new ArrayList<>(4);
+    this.addOnSubmitValidationListener(new OnSubmitValidationListener() {
+      @Override
+      public void preBranchUpdate(Arguments args) throws ValidationException {
+        assertThat(args.getCommands().keySet()).contains("refs/heads/master");
+        try (RevWalk rw = args.newRevWalk()) {
+          rw.parseBody(rw.parseCommit(
+              args.getCommands().get("refs/heads/master").getNewId()));
+        } catch (IOException e) {
+          assertThat(e).isNull();
+        }
+        projectsCalled.add(args.getProject().get());
+        if (projectsCalled.size() == 2) {
+          throw new ValidationException("time to fail");
+        }
+      }
+    });
+    submitWithConflict(change4.getChangeId(), "time to fail");
+    assertThat(projectsCalled).containsExactly(name("project-a"),
+        name("project-b"));
+    for (PushOneCommit.Result change : changes) {
+      change.assertChange(Change.Status.NEW, name(topic), admin);
+    }
+
+    submit(change4.getChangeId());
+    assertThat(projectsCalled).containsExactly(name("project-a"),
+        name("project-b"), name("project-a"), name("project-b"));
+    for (PushOneCommit.Result change : changes) {
+      change.assertChange(Change.Status.MERGED, name(topic), admin);
+    }
+  }
+
   private void setChangeStatusToNew(PushOneCommit.Result... changes)
       throws Exception {
     for (PushOneCommit.Result change : changes) {
@@ -881,6 +990,11 @@
     return getRemoteLog(project, "master");
   }
 
+  protected void addOnSubmitValidationListener(OnSubmitValidationListener listener){
+    assertThat(onSubmitValidatorHandle).isNull();
+    onSubmitValidatorHandle = onSubmitValidationListeners.add(listener);
+  }
+
   private String getLatestDiff(Repository repo) throws Exception {
     ObjectId oldTreeId = repo.resolve("HEAD~1^{tree}");
     ObjectId newTreeId = repo.resolve("HEAD^{tree}");
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
new file mode 100644
index 0000000..c36c44b
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/change/DeleteVoteIT.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.change;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.extensions.client.ReviewerState.REVIEWER;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.gson.reflect.TypeToken;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.extensions.api.changes.ReviewInput;
+import com.google.gerrit.extensions.common.AccountInfo;
+import com.google.gerrit.extensions.common.ChangeInfo;
+import com.google.gerrit.extensions.common.ChangeMessageInfo;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.testutil.FakeEmailSender;
+
+import org.junit.Test;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class DeleteVoteIT extends AbstractDaemonTest {
+  @Test
+  public void deleteVoteOnChange() throws Exception {
+    deleteVote(false);
+  }
+
+  @Test
+  public void deleteVoteOnRevision() throws Exception {
+    deleteVote(true);
+  }
+
+  private void deleteVote(boolean onRevisionLevel) throws Exception {
+    PushOneCommit.Result r = createChange();
+    gApi.changes()
+        .id(r.getChangeId())
+        .revision(r.getCommit().name())
+        .review(ReviewInput.approve());
+
+    PushOneCommit.Result r2 = amendChange(r.getChangeId());
+
+    setApiUser(user);
+    recommend(r.getChangeId());
+
+    sender.clear();
+    String endPoint = "/changes/" + r.getChangeId()
+        + (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "")
+        + "/reviewers/" + user.getId().toString()
+        + "/votes/Code-Review";
+
+    RestResponse response = adminRestSession.delete(endPoint);
+    response.assertNoContent();
+
+    List<FakeEmailSender.Message> messages = sender.getMessages();
+    assertThat(messages).hasSize(1);
+    FakeEmailSender.Message msg = messages.get(0);
+    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");
+
+    endPoint = "/changes/" + r.getChangeId()
+        + (onRevisionLevel ? ("/revisions/" + r2.getCommit().getName()) : "")
+        + "/reviewers/" + user.getId().toString()
+        + "/votes";
+
+    response = adminRestSession.get(endPoint);
+    response.assertOK();
+
+    Map<String, Short> m = newGson().fromJson(response.getReader(),
+            new TypeToken<Map<String, Short>>() {}.getType());
+
+    assertThat(m).containsExactly("Code-Review", Short.valueOf((short)0));
+
+    ChangeInfo c = gApi.changes()
+        .id(r.getChangeId())
+        .get();
+
+    ChangeMessageInfo message = Iterables.getLast(c.messages);
+    assertThat(message.author._accountId).isEqualTo(admin.getId().get());
+    assertThat(message.message).isEqualTo(
+        "Removed Code-Review+1 by User <user@example.com>\n");
+    assertThat(getReviewers(c.reviewers.get(REVIEWER)))
+        .containsExactlyElementsIn(
+            ImmutableSet.of(admin.getId(), user.getId()));
+  }
+
+  private Iterable<Account.Id> getReviewers(Collection<AccountInfo> r) {
+    return Iterables.transform(r, a -> new Account.Id(a._accountId));
+  }
+}
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 e33d163..fced72e 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
@@ -50,6 +50,7 @@
 import com.google.gerrit.server.git.BatchUpdate.RepoContext;
 import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.testutil.InMemoryRepositoryManager;
 import com.google.gerrit.testutil.TestChanges;
@@ -297,8 +298,10 @@
     String rev = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
     PatchSet ps = newPatchSet(psId, rev, adminId);
 
-    db.changes().insert(singleton(c));
-    db.patchSets().insert(singleton(ps));
+    if (notesMigration.changePrimaryStorage() == PrimaryStorage.REVIEW_DB) {
+      db.changes().insert(singleton(c));
+      db.patchSets().insert(singleton(ps));
+    }
     addNoteDbCommit(
         c.getId(),
         "Create change\n"
@@ -824,10 +827,12 @@
     Change c = new Change(ctl.getChange());
     PatchSet.Id psId = nextPatchSetId(ctl);
     c.setCurrentPatchSet(psId, subject, c.getOriginalSubject());
-
     PatchSet ps = newPatchSet(psId, rev, adminId);
-    db.patchSets().insert(singleton(ps));
-    db.changes().update(singleton(c));
+
+    if (PrimaryStorage.of(c) == PrimaryStorage.REVIEW_DB) {
+      db.patchSets().insert(singleton(ps));
+      db.changes().update(singleton(c));
+    }
 
     addNoteDbCommit(
         c.getId(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdCacheImpl.java
index 8585ffd..e55b15a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ExternalIdCacheImpl.java
@@ -160,7 +160,7 @@
     }
   }
 
-  static class AllKey {
+  public static class AllKey {
     static final AllKey ALL = new AllKey();
 
     private AllKey() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
index 131513b..0753769 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/SuggestChangeReviewers.java
@@ -15,10 +15,12 @@
 package com.google.gerrit.server.change;
 
 import com.google.gerrit.extensions.common.SuggestedReviewerInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.ReviewersUtil;
@@ -42,18 +44,25 @@
       usage = "exclude groups from query")
   boolean excludeGroups;
 
+  private final Provider<CurrentUser> self;
+
   @Inject
   SuggestChangeReviewers(AccountVisibility av,
       GenericFactory identifiedUserFactory,
       Provider<ReviewDb> dbProvider,
+      Provider<CurrentUser> self,
       @GerritServerConfig Config cfg,
       ReviewersUtil reviewersUtil) {
     super(av, identifiedUserFactory, dbProvider, cfg, reviewersUtil);
+    this.self = self;
   }
 
   @Override
   public List<SuggestedReviewerInfo> apply(ChangeResource rsrc)
-      throws BadRequestException, OrmException, IOException {
+      throws AuthException, BadRequestException, OrmException, IOException {
+    if (!self.get().isIdentifiedUser()) {
+      throw new AuthException("Authentication required");
+    }
     return reviewersUtil.suggestReviewers(rsrc.getNotes(), this,
         rsrc.getControl().getProjectControl(), getVisibility(rsrc), excludeGroups);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index df96a54..6012142 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -128,6 +128,8 @@
 import com.google.gerrit.server.git.validators.MergeValidators.ProjectConfigValidator;
 import com.google.gerrit.server.git.validators.RefOperationValidationListener;
 import com.google.gerrit.server.git.validators.RefOperationValidators;
+import com.google.gerrit.server.git.validators.OnSubmitValidationListener;
+import com.google.gerrit.server.git.validators.OnSubmitValidators;
 import com.google.gerrit.server.git.validators.UploadValidationListener;
 import com.google.gerrit.server.git.validators.UploadValidators;
 import com.google.gerrit.server.group.GroupInfoCache;
@@ -350,6 +352,7 @@
     DynamicSet.setOf(binder(), CommitValidationListener.class);
     DynamicSet.setOf(binder(), ChangeMessageModifier.class);
     DynamicSet.setOf(binder(), RefOperationValidationListener.class);
+    DynamicSet.setOf(binder(), OnSubmitValidationListener.class);
     DynamicSet.setOf(binder(), MergeValidationListener.class);
     DynamicSet.setOf(binder(), ProjectCreationValidationListener.class);
     DynamicSet.setOf(binder(), GroupCreationValidationListener.class);
@@ -391,6 +394,7 @@
 
     factory(AbandonOp.Factory.class);
     factory(RefOperationValidators.Factory.class);
+    factory(OnSubmitValidators.Factory.class);
     factory(MergeValidators.Factory.class);
     factory(ProjectConfigValidator.Factory.class);
     factory(NotesBranchUtil.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
index 5de4ff5..7419dab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BatchUpdate.java
@@ -51,6 +51,7 @@
 import com.google.gerrit.server.config.AllUsersName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.validators.OnSubmitValidators;
 import com.google.gerrit.server.index.change.ChangeIndexer;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.ChangeUpdate;
@@ -532,6 +533,7 @@
   private BatchRefUpdate batchRefUpdate;
   private boolean closeRepo;
   private Order order;
+  private OnSubmitValidators onSubmitValidators;
   private boolean updateChangesInParallel;
   private RequestId requestId;
 
@@ -607,6 +609,15 @@
   }
 
   /**
+   * Add a validation step for intended ref operations, which will be performed
+   * at the end of {@link RepoOnlyOp#updateRepo(RepoContext)} step.
+   */
+  BatchUpdate setOnSubmitValidators(OnSubmitValidators onSubmitValidators) {
+    this.onSubmitValidators = onSubmitValidators;
+    return this;
+  }
+
+  /**
    * Execute {@link Op#updateChange(ChangeContext)} in parallel for each change.
    */
   public BatchUpdate updateChangesInParallel() {
@@ -692,6 +703,18 @@
         op.updateRepo(ctx);
       }
 
+      if (onSubmitValidators != null && commands != null
+          && !commands.isEmpty()) {
+        // Validation of refs has to take place here and not at the beginning
+        // executeRefUpdates. Otherwise failing validation in a second
+        // BatchUpdate object will happen *after* first object's
+        // executeRefUpdates has finished, hence after first repo's refs have
+        // been updated, which is too late.
+        onSubmitValidators.validate(project,
+            new ReadOnlyRepository(getRepository()),
+            ctx.getInserter().newReader(), commands.getCommands());
+      }
+
       if (inserter != null) {
         logDebug("Flushing inserter");
         inserter.flush();
@@ -1049,18 +1072,45 @@
     private ChangeContext newChangeContext(ReviewDb db, Repository repo,
         RevWalk rw, Change.Id id) throws OrmException {
       Change c = newChanges.get(id);
-      if (c == null) {
+      boolean isNew = c != null;
+      PrimaryStorage defaultStorage = notesMigration.changePrimaryStorage();
+      if (isNew) {
+        // New change: populate noteDbState.
+        checkState(c.getNoteDbState() == null,
+            "noteDbState should not be filled in by callers");
+        if (defaultStorage == PrimaryStorage.NOTE_DB) {
+          c.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
+        }
+      } else {
+        // Existing change.
         c = ChangeNotes.readOneReviewDbChange(db, id);
         if (c == null) {
-          logDebug("Failed to get change {} from unwrapped db", id);
-          throw new NoSuchChangeException(id);
+          if (defaultStorage == PrimaryStorage.REVIEW_DB) {
+            logDebug("Failed to get change {} from unwrapped db", id);
+            throw new NoSuchChangeException(id);
+          }
+          // Not in ReviewDb, but new changes are created with default primary
+          // storage as NOTE_DB, so we can assume that a missing change is
+          // NoteDb primary. Pass a synthetic change into ChangeNotes.Factory,
+          // which lets ChangeNotes take care of the existence check.
+          //
+          // TODO(dborowitz): This assumption is potentially risky, because
+          // it means once we turn this option on and start creating changes
+          // without writing anything to ReviewDb, we can't turn this option
+          // back off without making those changes inaccessible. The problem
+          // is we have no way of distinguishing a change that only exists in
+          // NoteDb because it only ever existed in NoteDb, from a change that
+          // only exists in NoteDb because it used to exist in ReviewDb and
+          // deleting from ReviewDb succeeded but deleting from NoteDb failed.
+          //
+          // TODO(dborowitz): We actually still have that problem anyway. Maybe
+          // we need a cutoff timestamp? Or maybe we need to start leaving
+          // tombstones in ReviewDb?
+          c = ChangeNotes.Factory.newNoteDbOnlyChange(project, id);
         }
         NoteDbChangeState.checkNotReadOnly(c, skewMs);
       }
-      // Pass in preloaded change to controlFor, to avoid:
-      //  - reading from a db that does not belong to this update
-      //  - attempting to read a change that doesn't exist yet
-      ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c);
+      ChangeNotes notes = changeNotesFactory.createForBatchUpdate(c, !isNew);
       ChangeControl ctl = changeControlFactory.controlFor(notes, user);
       return new ChangeContext(ctl, new BatchUpdateReviewDb(db), repo, rw);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
index 5196ebe..1244ad3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOpRepoManager.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
+import com.google.gerrit.server.git.validators.OnSubmitValidators;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
@@ -107,7 +108,8 @@
       if (update == null) {
         update = batchUpdateFactory.create(db, getProjectName(), caller, ts)
             .setRepository(repo, rw, ins)
-            .setRequestId(submissionId);
+            .setRequestId(submissionId)
+            .setOnSubmitValidators(onSubmitValidatorsFactory.create());
       }
       return update;
     }
@@ -157,6 +159,7 @@
 
   private final Map<Project.NameKey, OpenRepo> openRepos;
   private final BatchUpdate.Factory batchUpdateFactory;
+  private final OnSubmitValidators.Factory onSubmitValidatorsFactory;
   private final GitRepositoryManager repoManager;
   private final ProjectCache projectCache;
 
@@ -169,10 +172,12 @@
   MergeOpRepoManager(
       GitRepositoryManager repoManager,
       ProjectCache projectCache,
-      BatchUpdate.Factory batchUpdateFactory) {
+      BatchUpdate.Factory batchUpdateFactory,
+      OnSubmitValidators.Factory onSubmitValidatorsFactory) {
     this.repoManager = repoManager;
     this.projectCache = projectCache;
     this.batchUpdateFactory = batchUpdateFactory;
+    this.onSubmitValidatorsFactory = onSubmitValidatorsFactory;
 
     openRepos = new HashMap<>();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
index e0e2ae7..692bc98 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/strategy/SubmitStrategy.java
@@ -46,6 +46,7 @@
 import com.google.gerrit.server.git.MergeUtil;
 import com.google.gerrit.server.git.SubmoduleOp;
 import com.google.gerrit.server.git.TagCache;
+import com.google.gerrit.server.git.validators.OnSubmitValidators;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectCache;
@@ -118,6 +119,7 @@
     final ProjectCache projectCache;
     final PersonIdent serverIdent;
     final RebaseChangeOp.Factory rebaseFactory;
+    final OnSubmitValidators.Factory onSubmitValidatorsFactory;
     final TagCache tagCache;
 
     final Branch.NameKey destBranch;
@@ -158,6 +160,7 @@
         @GerritPersonIdent PersonIdent serverIdent,
         ProjectCache projectCache,
         RebaseChangeOp.Factory rebaseFactory,
+        OnSubmitValidators.Factory onSubmitValidatorsFactory,
         TagCache tagCache,
         @Assisted Branch.NameKey destBranch,
         @Assisted CommitStatus commits,
@@ -212,6 +215,7 @@
             "project not found: %s", destBranch.getParentKey());
       this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag);
       this.mergeUtil = mergeUtilFactory.create(project);
+      this.onSubmitValidatorsFactory = onSubmitValidatorsFactory;
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java
new file mode 100644
index 0000000..f2f397c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidationListener.java
@@ -0,0 +1,85 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.git.validators;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.server.validators.ValidationException;
+
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+import java.util.Map;
+
+/**
+ * Listener to validate ref updates performed during submit operation.
+ *
+ * As submit strategies may generate new commits (e.g. Cherry Pick), this
+ * listener allows validation of resulting new commit before destination branch
+ * is updated and new patchset ref is created.
+ *
+ * If you only care about validating the change being submitted and not the
+ * resulting new commit, consider using {@link MergeValidationListener} instead.
+ */
+@ExtensionPoint
+public interface OnSubmitValidationListener {
+  public class Arguments {
+    private Project.NameKey project;
+    private Repository repository;
+    private ObjectReader objectReader;
+    private Map<String, ReceiveCommand> commands;
+
+    public Arguments(NameKey project, Repository repository,
+        ObjectReader objectReader, Map<String, ReceiveCommand> commands) {
+      this.project = project;
+      this.repository = repository;
+      this.objectReader = objectReader;
+      this.commands = commands;
+    }
+
+    public Project.NameKey getProject() {
+      return project;
+    }
+
+    /**
+     * @return a read only repository
+     */
+    public Repository getRepository() {
+      return repository;
+    }
+
+    public RevWalk newRevWalk() {
+      return new RevWalk(objectReader);
+    }
+
+    /**
+     * @return a map from ref to op on it covering all ref ops to be performed
+     *         on this repository as part of ongoing submit operation.
+     */
+    public Map<String, ReceiveCommand> getCommands(){
+      return commands;
+    }
+  }
+
+  /**
+   * Called right before branch is updated with new commit or commits as a
+   * result of submit.
+   *
+   * If ValidationException is thrown, submitting is aborted.
+   */
+  void preBranchUpdate(Arguments args) throws ValidationException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java
new file mode 100644
index 0000000..568c597
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/OnSubmitValidators.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.server.git.validators;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.IntegrationException;
+import com.google.gerrit.server.git.validators.OnSubmitValidationListener.Arguments;
+import com.google.gerrit.server.validators.ValidationException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+import java.util.Map;
+
+public class OnSubmitValidators {
+  public interface Factory {
+    OnSubmitValidators create();
+  }
+
+  private final DynamicSet<OnSubmitValidationListener> listeners;
+
+  @Inject
+  OnSubmitValidators(DynamicSet<OnSubmitValidationListener> listeners) {
+    this.listeners = listeners;
+  }
+
+  public void validate(Project.NameKey project, Repository repo,
+      ObjectReader objectReader, Map<String, ReceiveCommand> commands)
+      throws IntegrationException {
+    try {
+      for (OnSubmitValidationListener listener : this.listeners) {
+        listener.preBranchUpdate(
+            new Arguments(project, repo, objectReader, commands));
+      }
+    } catch (ValidationException e) {
+      throw new IntegrationException(e.getMessage());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
index c00035c..c58d3fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/AbstractChangeNotes.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
 import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -174,7 +175,20 @@
     return ref != null ? ref.getObjectId() : null;
   }
 
-  protected LoadHandle openHandle(Repository repo) throws IOException {
+  /**
+   * Open a handle for reading this entity from a repository.
+   * <p>
+   * Implementations may override this method to provide auto-rebuilding
+   * behavior.
+   *
+   * @param repo open repository.
+   * @return handle for reading the entity.
+   *
+   * @throws NoSuchChangeException change does not exist.
+   * @throws IOException a repo-level error occurred.
+   */
+  protected LoadHandle openHandle(Repository repo)
+      throws NoSuchChangeException, IOException {
     return openHandle(repo, readRef(repo));
   }
 
@@ -182,7 +196,7 @@
     return LoadHandle.create(ChangeNotesCommit.newRevWalk(repo), id);
   }
 
-  public T reload() throws OrmException {
+  public T reload() throws NoSuchChangeException, OrmException {
     loaded = false;
     return load();
   }
@@ -215,7 +229,7 @@
 
   /** Set up the metadata, parsing any state from the loaded revision. */
   protected abstract void onLoad(LoadHandle handle)
-      throws IOException, ConfigInvalidException;
+      throws NoSuchChangeException, IOException, ConfigInvalidException;
 
   @SuppressWarnings("unchecked")
   protected final T self() {
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 495641d..ee9f4f7 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
@@ -36,6 +36,7 @@
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.metrics.Timer1;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.Comment;
@@ -124,7 +125,14 @@
     public ChangeNotes createChecked(ReviewDb db, Project.NameKey project,
         Change.Id changeId) throws OrmException {
       Change change = readOneReviewDbChange(db, changeId);
-      if (change == null || !change.getProject().equals(project)) {
+      if (change == null) {
+        if (!args.migration.readChanges()) {
+          throw new NoSuchChangeException(changeId);
+        }
+        // Change isn't in ReviewDb, but its primary storage might be in NoteDb.
+        // Prepopulate the change exists with proper noteDbState field.
+        change = newNoteDbOnlyChange(project, changeId);
+      } else if (!change.getProject().equals(project)) {
         throw new NoSuchChangeException(changeId);
       }
       return new ChangeNotes(args, change).load();
@@ -144,16 +152,33 @@
       return changes.get(0).notes();
     }
 
+    public static Change newNoteDbOnlyChange(
+        Project.NameKey project, Change.Id changeId) {
+      Change change = new Change(
+          null, changeId, null,
+          new Branch.NameKey(project, "INVALID_NOTE_DB_ONLY"),
+          null);
+      change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE);
+      return change;
+    }
+
     private Change loadChangeFromDb(ReviewDb db, Project.NameKey project,
         Change.Id changeId) throws OrmException {
-      Change change = readOneReviewDbChange(db, changeId);
       checkArgument(project != null, "project is required");
-      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",
-          project, changeId, change.getProject());
+      Change change = readOneReviewDbChange(db, changeId);
+
+      if (change == null && args.migration.readChanges()) {
+        // Change isn't in ReviewDb, but its primary storage might be in NoteDb.
+        // Prepopulate the change exists with proper noteDbState field.
+        change = newNoteDbOnlyChange(project, changeId);
+      } else {
+        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",
+            project, changeId, change.getProject());
+      }
+
       // TODO: Throw NoSuchChangeException when the change is not found in the
       // database
       return change;
@@ -168,7 +193,7 @@
     public ChangeNotes createWithAutoRebuildingDisabled(ReviewDb db,
         Project.NameKey project, Change.Id changeId) throws OrmException {
       return new ChangeNotes(
-          args, loadChangeFromDb(db, project, changeId), false, null).load();
+          args, loadChangeFromDb(db, project, changeId), true, false, null).load();
     }
 
     /**
@@ -183,13 +208,14 @@
       return new ChangeNotes(args, change);
     }
 
-    public ChangeNotes createForBatchUpdate(Change change) throws OrmException {
-      return new ChangeNotes(args, change, false, null).load();
+    public ChangeNotes createForBatchUpdate(Change change, boolean shouldExist)
+        throws OrmException {
+      return new ChangeNotes(args, change, shouldExist, false, null).load();
     }
 
     public ChangeNotes createWithAutoRebuildingDisabled(Change change,
         RefCache refs) throws OrmException {
-      return new ChangeNotes(args, change, false, refs).load();
+      return new ChangeNotes(args, change, true, false, refs).load();
     }
 
     // TODO(ekempin): Remove when database backend is deleted
@@ -226,7 +252,7 @@
 
     public List<ChangeNotes> create(ReviewDb db, Project.NameKey project,
         Collection<Change.Id> changeIds, Predicate<ChangeNotes> predicate)
-            throws OrmException {
+        throws OrmException {
       List<ChangeNotes> notes = new ArrayList<>();
       if (args.migration.enabled()) {
         for (Change.Id cid : changeIds) {
@@ -302,13 +328,18 @@
         Project.NameKey project) throws OrmException, IOException {
       Set<Change.Id> ids = scan(repo);
       List<ChangeNotes> changeNotes = new ArrayList<>(ids.size());
+      PrimaryStorage defaultStorage = args.migration.changePrimaryStorage();
       for (Change.Id id : ids) {
         Change change = readOneReviewDbChange(db, id);
         if (change == null) {
-          log.warn("skipping change {} found in project {} " +
-              "but not in ReviewDb",
-              id, project);
-          continue;
+          if (defaultStorage == PrimaryStorage.REVIEW_DB) {
+            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 {} " +
@@ -337,6 +368,7 @@
     }
   }
 
+  private final boolean shouldExist;
   private final RefCache refs;
 
   private Change change;
@@ -358,13 +390,14 @@
 
   @VisibleForTesting
   public ChangeNotes(Args args, Change change) {
-    this(args, change, true, null);
+    this(args, change, true, true, null);
   }
 
-  private ChangeNotes(Args args, Change change, boolean autoRebuild,
-      @Nullable RefCache refs) {
+  private ChangeNotes(Args args, Change change, boolean shouldExist,
+      boolean autoRebuild, @Nullable RefCache refs) {
     super(args, change.getId(), PrimaryStorage.of(change), autoRebuild);
     this.change = new Change(change);
+    this.shouldExist = shouldExist;
     this.refs = refs;
   }
 
@@ -555,9 +588,14 @@
 
   @Override
   protected void onLoad(LoadHandle handle)
-      throws IOException, ConfigInvalidException {
+      throws NoSuchChangeException, IOException, ConfigInvalidException {
     ObjectId rev = handle.id();
     if (rev == null) {
+      if (args.migration.readChanges()
+          && PrimaryStorage.of(change) == PrimaryStorage.NOTE_DB
+          && shouldExist) {
+        throw new NoSuchChangeException(getChangeId());
+      }
       loadDefaults();
       return;
     }
@@ -587,12 +625,17 @@
   }
 
   @Override
-  protected LoadHandle openHandle(Repository repo) throws IOException {
+  protected LoadHandle openHandle(Repository repo)
+      throws NoSuchChangeException, IOException {
     if (autoRebuild) {
       NoteDbChangeState state = NoteDbChangeState.parse(change);
       ObjectId id = readRef(repo);
-      if (state == null && id == null) {
-        return super.openHandle(repo, id);
+      if (id == null) {
+        if (state == null) {
+          return super.openHandle(repo, id);
+        } else if (shouldExist) {
+          throw new NoSuchChangeException(getChangeId());
+        }
       }
       RefCache refs = this.refs != null ? this.refs : new RepoRefCache(repo);
       if (!NoteDbChangeState.isChangeUpToDate(state, refs, getChangeId())) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
index 802359c..bf9bc4e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/ConfigNotesMigration.java
@@ -20,6 +20,7 @@
 
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -49,9 +50,11 @@
   }
 
   private static final String NOTE_DB = "noteDb";
+
+  private static final String PRIMARY_STORAGE = "primaryStorage";
   private static final String READ = "read";
-  private static final String WRITE = "write";
   private static final String SEQUENCE = "sequence";
+  private static final String WRITE = "write";
 
   private static void checkConfig(Config cfg) {
     Set<String> keys = new HashSet<>();
@@ -81,6 +84,7 @@
   private final boolean writeChanges;
   private final boolean readChanges;
   private final boolean readChangeSequence;
+  private final PrimaryStorage changePrimaryStorage;
 
   private final boolean writeAccounts;
   private final boolean readAccounts;
@@ -98,6 +102,9 @@
     // NoteDb. This decision for the default may be reevaluated later.
     readChangeSequence = cfg.getBoolean(NOTE_DB, CHANGES.key(), SEQUENCE, false);
 
+    changePrimaryStorage = cfg.getEnum(
+        NOTE_DB, CHANGES.key(), PRIMARY_STORAGE, PrimaryStorage.REVIEW_DB);
+
     writeAccounts = cfg.getBoolean(NOTE_DB, ACCOUNTS.key(), WRITE, false);
     readAccounts = cfg.getBoolean(NOTE_DB, ACCOUNTS.key(), READ, false);
   }
@@ -118,6 +125,11 @@
   }
 
   @Override
+  public PrimaryStorage changePrimaryStorage() {
+    return changePrimaryStorage;
+  }
+
+  @Override
   public boolean writeAccounts() {
     return writeAccounts;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
index 4feac2e..0408ffa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/DraftCommentNotes.java
@@ -188,7 +188,8 @@
   }
 
   @Override
-  protected LoadHandle openHandle(Repository repo) throws IOException {
+  protected LoadHandle openHandle(Repository repo)
+      throws NoSuchChangeException, IOException {
     if (rebuildResult != null) {
       StagedResult sr = checkNotNull(rebuildResult.staged());
       return LoadHandle.create(
@@ -216,7 +217,8 @@
     return null;
   }
 
-  private LoadHandle rebuildAndOpen(Repository repo) throws IOException {
+  private LoadHandle rebuildAndOpen(Repository repo)
+      throws NoSuchChangeException, IOException {
     Timer1.Context timer = args.metrics.autoRebuildLatency.start(CHANGES);
     try {
       Change.Id cid = getChangeId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
index 56b41d9..6afe87d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/notedb/NotesMigration.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.notedb;
 
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+
 /**
  * Holds the current state of the NoteDb migration.
  * <p>
@@ -71,6 +73,9 @@
    */
   public abstract boolean readChangeSequence();
 
+  /** @return default primary storage for new changes. */
+  public abstract PrimaryStorage changePrimaryStorage();
+
   public abstract boolean readAccounts();
 
   public abstract boolean writeAccounts();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index f9dd576..bc107ca 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -60,6 +60,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PatchSetUtil;
 import com.google.gerrit.server.Sequences;
 import com.google.gerrit.server.StarredChangesUtil;
 import com.google.gerrit.server.account.AccountManager;
@@ -146,6 +147,7 @@
   @Inject protected InternalChangeQuery internalChangeQuery;
   @Inject protected ChangeNotes.Factory notesFactory;
   @Inject protected PatchSetInserter.Factory patchSetFactory;
+  @Inject protected PatchSetUtil psUtil;
   @Inject protected ChangeControl.GenericFactory changeControlFactory;
   @Inject protected ChangeQueryProcessor queryProcessor;
   @Inject protected SchemaCreator schemaCreator;
@@ -1579,9 +1581,13 @@
     Account.Id user2 = createAccount("user2");
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo));
-    PatchSet ps1 = db.patchSets().get(change1.currentPatchSetId());
+    ChangeNotes notes1 =
+        notesFactory.create(db, change1.getProject(), change1.getId());
+    PatchSet ps1 = psUtil.get(db, notes1, change1.currentPatchSetId());
     Change change2 = insert(repo, newChange(repo));
-    PatchSet ps2 = db.patchSets().get(change2.currentPatchSetId());
+    ChangeNotes notes2 =
+        notesFactory.create(db, change2.getProject(), change2.getId());
+    PatchSet ps2 = psUtil.get(db, notes2, change2.currentPatchSetId());
 
     requestContext.setContext(newRequestContext(user1));
     assertQuery("has:edit");
@@ -1692,7 +1698,9 @@
     Project.NameKey project = new Project.NameKey("repo");
     TestRepository<Repo> repo = createProject(project.get());
     Change change = insert(repo, newChange(repo));
-    PatchSet ps = db.patchSets().get(change.currentPatchSetId());
+    ChangeNotes notes =
+        notesFactory.create(db, change.getProject(), change.getId());
+    PatchSet ps = psUtil.get(db, notes, change.currentPatchSetId());
 
     requestContext.setContext(newRequestContext(user));
     assertThat(changeEditModifier.createEdit(change, ps))
@@ -1714,6 +1722,9 @@
 
   @Test
   public void refStateFields() throws Exception {
+    // This test method manages primary storage manually.
+    assume().that(notesMigration.changePrimaryStorage())
+        .isEqualTo(PrimaryStorage.REVIEW_DB);
     Account.Id user = createAccount("user");
     Project.NameKey project = new Project.NameKey("repo");
     TestRepository<Repo> repo = createProject(project.get());
@@ -1723,7 +1734,9 @@
     Change change = insert(repo, newChangeForCommit(repo, commit));
     Change.Id id = change.getId();
     int c = id.get();
-    PatchSet ps = db.patchSets().get(change.currentPatchSetId());
+    ChangeNotes notes =
+        notesFactory.create(db, change.getProject(), change.getId());
+    PatchSet ps = psUtil.get(db, notes, change.currentPatchSetId());
     requestContext.setContext(newRequestContext(user));
 
     // Ensure one of each type of supported ref is present for the change. If
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
index c5f4301..ae0e515 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbChecker.java
@@ -39,6 +39,7 @@
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
+import org.junit.runner.Description;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -124,6 +125,25 @@
     }
   }
 
+  public void assertNoReviewDbChanges(Description desc) throws Exception {
+    ReviewDb db = getUnwrappedDb();
+    assertThat(db.changes().all().toList())
+        .named("Changes in " + desc.getTestClass())
+        .isEmpty();
+    assertThat(db.changeMessages().all().toList())
+        .named("ChangeMessages in " + desc.getTestClass())
+        .isEmpty();
+    assertThat(db.patchSets().all().toList())
+        .named("PatchSets in " + desc.getTestClass())
+        .isEmpty();
+    assertThat(db.patchSetApprovals().all().toList())
+        .named("PatchSetApprovals in " + desc.getTestClass())
+        .isEmpty();
+    assertThat(db.patchComments().all().toList())
+        .named("PatchLineComments in " + desc.getTestClass())
+        .isEmpty();
+  }
+
   private List<ChangeBundle> readExpected(Stream<Change.Id> changeIds)
       throws Exception {
     boolean old = notesMigration.readChanges();
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java
index 259973d..7db76fd 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/NoteDbMode.java
@@ -29,6 +29,9 @@
   /** Reading and writing all data to NoteDb is enabled. */
   READ_WRITE,
 
+  /** Changes are created with their primary storage as NoteDb. */
+  PRIMARY,
+
   /**
    * Run tests with NoteDb disabled, then convert ReviewDb to NoteDb and check
    * that the results match.
@@ -59,6 +62,6 @@
   }
 
   public static boolean readWrite() {
-    return get() == READ_WRITE;
+    return get() == READ_WRITE || get() == PRIMARY;
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
index 2f9d67f..b11b2cd 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/TestNotesMigration.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.testutil;
 
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.notedb.NotesMigration;
 import com.google.inject.Singleton;
 
@@ -22,6 +25,8 @@
 public class TestNotesMigration extends NotesMigration {
   private volatile boolean readChanges;
   private volatile boolean writeChanges;
+  private volatile PrimaryStorage changePrimaryStorage =
+      PrimaryStorage.REVIEW_DB;
   private volatile boolean failOnLoad;
 
   @Override
@@ -36,6 +41,11 @@
     return readChanges;
   }
 
+  @Override
+  public PrimaryStorage changePrimaryStorage() {
+    return changePrimaryStorage;
+  }
+
   // Increase visbility from superclass, as tests may want to check whether
   // NoteDb data is written in specific migration scenarios.
   @Override
@@ -68,6 +78,12 @@
     return this;
   }
 
+  public TestNotesMigration setChangePrimaryStorage(
+      PrimaryStorage changePrimaryStorage) {
+    this.changePrimaryStorage = checkNotNull(changePrimaryStorage);
+    return this;
+  }
+
   public TestNotesMigration setFailOnLoad(boolean failOnLoad) {
     this.failOnLoad = failOnLoad;
     return this;
@@ -82,16 +98,24 @@
       case READ_WRITE:
         setWriteChanges(true);
         setReadChanges(true);
+        setChangePrimaryStorage(PrimaryStorage.REVIEW_DB);
         break;
       case WRITE:
         setWriteChanges(true);
         setReadChanges(false);
+        setChangePrimaryStorage(PrimaryStorage.REVIEW_DB);
+        break;
+      case PRIMARY:
+        setWriteChanges(true);
+        setReadChanges(true);
+        setChangePrimaryStorage(PrimaryStorage.NOTE_DB);
         break;
       case CHECK:
       case OFF:
       default:
         setWriteChanges(false);
         setReadChanges(false);
+        setChangePrimaryStorage(PrimaryStorage.REVIEW_DB);
         break;
     }
     return this;
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index 4bd5141..099eb9e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -253,7 +253,17 @@
           <gr-change-star id="changeStar"
               change="{{_change}}" hidden$="[[!_loggedIn]]"></gr-change-star>
           <a href$="[[_computeChangePermalink(_change._number)]]">[[_change._number]]</a><!--
-       --><span class="changeStatus">[[_computeChangeStatus(_change, _patchRange.patchNum)]]</span><!--
+       --><template is="dom-if" if="[[_changeStatus]]">
+            ([[_changeStatus]]<!--
+         --><template is="dom-if" if="[[_computeShowCommitInfo(_changeStatus)]]">
+              as
+              <gr-commit-info
+                change="[[_change]]"
+                commit-info="[[_computeMergedCommitInfo(_change.current_revision, _change.revisions)]]"
+                server-config="[[serverConfig]]"></gr-commit-info><!--
+         --></template><!--
+       -->)<!--
+     --></template><!--
        -->:
           [[_change.subject]]
         </span>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
index 910df6d..d66917c 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.js
@@ -127,6 +127,10 @@
         value: true,
         computed: '_computeReplyDisabled(serverConfig)',
       },
+      _changeStatus: {
+        type: String,
+        computed: '_computeChangeStatus(_change, _patchRange.patchNum)',
+      },
     },
 
     behaviors: [
@@ -556,7 +560,17 @@
       } else {
         statusString = this.changeStatusString(change);
       }
-      return statusString ? ' (' + statusString + ')' : '';
+      return statusString || '';
+    },
+
+    _computeShowCommitInfo: function(changeStatus) {
+      return changeStatus === 'Merged';
+    },
+
+    _computeMergedCommitInfo: function(current_revision, revisions) {
+      var rev = revisions[current_revision];
+      if (!rev) { return {}; }
+      return rev.commit;
     },
 
     _computeChangeIdClass: function(displayChangeId) {
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
index dafff75..4f2731a 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.html
@@ -603,7 +603,26 @@
         labels: {},
       };
       var status = element._computeChangeStatus(element._change, '1');
-      assert.equal(status, ' (Draft)');
+      assert.equal(status, 'Draft');
+    });
+
+    test('change status merged', function() {
+      element._changeNum = '1';
+      element._patchRange = {
+        basePatchNum: 'PARENT',
+        patchNum: 1,
+      };
+      element._change = {
+        change_id: 'Iad9dc96274af6946f3632be53b106ef80f7ba6ca',
+        revisions: {
+          rev1: {_number: 1},
+        },
+        current_revision: 'rev1',
+        status: element.ChangeStatus.MERGED,
+        labels: {},
+      };
+      var status = element._computeChangeStatus(element._change, '1');
+      assert.equal(status, 'Merged');
     });
 
     test('revision status draft', function() {
@@ -626,7 +645,15 @@
         labels: {},
       };
       var status = element._computeChangeStatus(element._change, '2');
-      assert.equal(status, ' (Draft)');
+      assert.equal(status, 'Draft');
+    });
+
+    test('_computeMergedCommitInfo', function() {
+      var dummyRevObj = {
+        sha: {commit: 'test'},
+      };
+      assert.deepEqual(element._computeMergedCommitInfo('sha', dummyRevObj),
+          'test');
     });
 
     test('get latest revision', function() {