Merge "Update buck to latest version"
diff --git a/WORKSPACE b/WORKSPACE
index 3c8d851..a3db958 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -415,8 +415,8 @@
 
 maven_jar(
   name = 'auto_value',
-  artifact = 'com.google.auto.value:auto-value:1.3-rc1',
-  sha1 = 'b764e0fb7e11353fbff493b22fd6e83bf091a179',
+  artifact = 'com.google.auto.value:auto-value:1.4-rc1',
+  sha1 = '9347939002003a7a3c3af48271fc2c18734528a4',
 )
 
 maven_jar(
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
index b9e4d7d..e1771ce 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/server/notedb/ChangeRebuilderIT.java
@@ -69,6 +69,7 @@
 import com.google.gerrit.server.notedb.ChangeBundleReader;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import com.google.gerrit.server.notedb.NoteDbChangeState;
+import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
 import com.google.gerrit.server.notedb.NoteDbUpdateManager;
 import com.google.gerrit.server.notedb.TestChangeRebuilderWrapper;
 import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder.NoPatchSetsException;
@@ -100,6 +101,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.concurrent.TimeUnit;
 
 public class ChangeRebuilderIT extends AbstractDaemonTest {
@@ -593,11 +595,15 @@
     ReviewDb db = getUnwrappedDb();
     Change c = db.changes().get(id);
     // Leave change meta ID alone so DraftCommentNotes does the rebuild.
+    ObjectId badSha =
+        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
     NoteDbChangeState bogusState = new NoteDbChangeState(
-        id, NoteDbChangeState.parse(c).getChangeMetaId(),
-        ImmutableMap.<Account.Id, ObjectId>of(
-            user.getId(),
-            ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")));
+        id,
+        PrimaryStorage.REVIEW_DB,
+        Optional.of(
+          NoteDbChangeState.RefState.create(
+              NoteDbChangeState.parse(c).getChangeMetaId(),
+              ImmutableMap.of(user.getId(), badSha))));
     c.setNoteDbState(bogusState.toString());
     db.changes().update(Collections.singleton(c));
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index 000e5fd..066464a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -27,11 +27,11 @@
 
 public class SearchSuggestOracle extends HighlightSuggestOracle {
   private static final List<ParamSuggester> paramSuggester = Arrays.asList(
-      new ParamSuggester(Arrays.asList("project:", "parentproject:"),
+      new ParamSuggester(Arrays.asList("project:", "p:", "parentproject:"),
           new ProjectNameSuggestOracle()),
       new ParamSuggester(Arrays.asList(
-          "owner:", "reviewer:", "commentby:", "reviewedby:", "author:",
-          "committer:", "from:", "assignee:"),
+          "owner:", "o:", "reviewer:", "r:", "commentby:", "reviewedby:",
+          "author:", "committer:", "from:", "assignee:"),
           new AccountSuggestOracle() {
             @Override
             public void onRequestSuggestions(final Request request, final Callback done) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
index 2956ffc..f1489bb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/Assignee.java
@@ -98,7 +98,6 @@
     this.changeId = info.legacyId();
     this.canEdit = info.hasActions() && info.actions().containsKey("assignee");
     setAssignee(info.assignee());
-    assigneeSuggestOracle.setChange(changeId);
     editAssigneeIcon.setVisible(canEdit);
     if (!canEdit) {
       show.setTitle(null);
@@ -111,8 +110,10 @@
     UIObject.setVisible(error, false);
     editAssigneeIcon.setVisible(false);
     suggestBox.setFocus(true);
-    suggestBox.setText(FormatUtil.nameEmail(currentAssignee));
-    suggestBox.selectAll();
+    if (currentAssignee != null) {
+      suggestBox.setText(FormatUtil.nameEmail(currentAssignee));
+      suggestBox.selectAll();
+    }
   }
 
   void onCloseForm() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AssigneeSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AssigneeSuggestOracle.java
index 8dc5574..47d7541 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AssigneeSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/change/AssigneeSuggestOracle.java
@@ -14,13 +14,12 @@
 
 package com.google.gerrit.client.change;
 
-import com.google.gerrit.client.change.ReviewerSuggestOracle.RestReviewerSuggestion;
-import com.google.gerrit.client.change.ReviewerSuggestOracle.SuggestReviewerInfo;
-import com.google.gerrit.client.changes.ChangeApi;
+import com.google.gerrit.client.account.AccountApi;
+import com.google.gerrit.client.info.AccountInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.ui.AccountSuggestOracle.AccountSuggestion;
 import com.google.gerrit.client.ui.SuggestAfterTypingNCharsOracle;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.JsArray;
 
 import java.util.ArrayList;
@@ -29,31 +28,24 @@
 
 /** REST API based suggestion Oracle for assignee */
 public class AssigneeSuggestOracle extends SuggestAfterTypingNCharsOracle {
-  private Change.Id changeId;
-
-  public void setChange(Change.Id changeId) {
-    this.changeId = changeId;
-  }
-
   @Override
   protected void _onRequestSuggestions(Request req, Callback cb) {
-    ChangeApi
-    .suggestReviewers(changeId.get(), req.getQuery(), req.getLimit(), true)
-    .get(new GerritCallback<JsArray<SuggestReviewerInfo>>() {
-      @Override
-      public void onSuccess(JsArray<SuggestReviewerInfo> result) {
-        List<RestReviewerSuggestion> r = new ArrayList<>(result.length());
-        for (SuggestReviewerInfo reviewer : Natives.asList(result)) {
-          r.add(new RestReviewerSuggestion(reviewer, req.getQuery()));
-        }
-        cb.onSuggestionsReady(req, new Response(r));
-      }
+    AccountApi.suggest(req.getQuery(), req.getLimit(),
+        new GerritCallback<JsArray<AccountInfo>>() {
+          @Override
+          public void onSuccess(JsArray<AccountInfo> result) {
+            List<AccountSuggestion> r = new ArrayList<>(result.length());
+            for (AccountInfo reviewer : Natives.asList(result)) {
+              r.add(new AccountSuggestion(reviewer, req.getQuery()));
+            }
+            cb.onSuggestionsReady(req, new Response(r));
+          }
 
-      @Override
-      public void onFailure(Throwable err) {
-        List<Suggestion> r = Collections.emptyList();
-        cb.onSuggestionsReady(req, new Response(r));
-      }
-    });
+          @Override
+          public void onFailure(Throwable err) {
+            List<Suggestion> r = Collections.emptyList();
+            cb.onSuggestionsReady(req, new Response(r));
+          }
+        });
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
index bfeeaec..e43a24e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
@@ -45,7 +45,7 @@
   public static class AccountSuggestion implements SuggestOracle.Suggestion {
     private final String suggestion;
 
-    AccountSuggestion(AccountInfo info, String query) {
+    public AccountSuggestion(AccountInfo info, String query) {
       this.suggestion = format(info, query);
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java
index 2d7736b..cab29da 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java
@@ -29,7 +29,8 @@
 
   @Override
   protected void onRequestSuggestions(Request req, Callback cb) {
-    if (req.getQuery().length() >= Gerrit.info().suggest().from()) {
+    if (req.getQuery() != null
+        && req.getQuery().length() >= Gerrit.info().suggest().from()) {
       _onRequestSuggestions(req, cb);
     } else {
       List<Suggestion> none = Collections.emptyList();
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 61f6557..dc229b8 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
@@ -44,7 +44,6 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.reviewdb.server.ReviewDbWrapper;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -1014,7 +1013,7 @@
         RevWalk rw, Change.Id id) throws Exception {
       Change c = newChanges.get(id);
       if (c == null) {
-        c = ReviewDbUtil.unwrapDb(db).changes().get(id);
+        c = ChangeNotes.readOneReviewDbChange(db, id);
       }
       // Pass in preloaded change to controlFor, to avoid:
       //  - reading from a db that does not belong to this update
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 68be2c5..eda50d7 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
@@ -52,6 +52,7 @@
 import com.google.gerrit.server.ReviewerStatusUpdate;
 import com.google.gerrit.server.git.RefCache;
 import com.google.gerrit.server.git.RepoRefCache;
+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.gerrit.server.project.ProjectCache;
@@ -95,6 +96,20 @@
         + String.format(fmt, args));
   }
 
+  public static Change readOneReviewDbChange(ReviewDb db, Change.Id id)
+      throws OrmException {
+    return checkNoteDbState(ReviewDbUtil.unwrapDb(db).changes().get(id));
+  }
+
+  private static Change checkNoteDbState(Change c) throws OrmException {
+    NoteDbChangeState s = NoteDbChangeState.parse(c);
+    if (s != null && s.getPrimaryStorage() != PrimaryStorage.REVIEW_DB) {
+      throw new OrmException(
+          "invalid NoteDbChangeState in " + c.getId() + ": " + s);
+    }
+    return c;
+  }
+
   @Singleton
   public static class Factory {
     private final Args args;
@@ -118,7 +133,7 @@
 
     public ChangeNotes createChecked(ReviewDb db, Project.NameKey project,
         Change.Id changeId) throws OrmException, NoSuchChangeException {
-      Change change = ReviewDbUtil.unwrapDb(db).changes().get(changeId);
+      Change change = readOneReviewDbChange(db, changeId);
       if (change == null || !change.getProject().equals(project)) {
         throw new NoSuchChangeException(changeId);
       }
@@ -142,7 +157,7 @@
 
     private Change loadChangeFromDb(ReviewDb db, Project.NameKey project,
         Change.Id changeId) throws OrmException {
-      Change change = ReviewDbUtil.unwrapDb(db).changes().get(changeId);
+      Change change = readOneReviewDbChange(db, changeId);
       checkArgument(project != null, "project is required");
       checkNotNull(change,
           "change %s not found in ReviewDb", changeId);
@@ -261,6 +276,7 @@
         }
       } else {
         for (Change change : ReviewDbUtil.unwrapDb(db).changes().all()) {
+          checkNoteDbState(change);
           ChangeNotes notes = createFromChangeOnlyWhenNoteDbDisabled(change);
           if (predicate.test(notes)) {
             m.put(change.getProject(), notes);
@@ -297,9 +313,8 @@
         Project.NameKey project) throws OrmException, IOException {
       Set<Change.Id> ids = scan(repo);
       List<ChangeNotes> changeNotes = new ArrayList<>(ids.size());
-      db = ReviewDbUtil.unwrapDb(db);
       for (Change.Id id : ids) {
-        Change change = db.changes().get(id);
+        Change change = readOneReviewDbChange(db, id);
         if (change == null) {
           log.warn("skipping change {} found in project {} " +
               "but not in ReviewDb",
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 ad54f02..ba9aca7 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
@@ -16,26 +16,28 @@
 
 import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
 import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef;
 import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments;
-import static java.util.Comparator.comparing;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.NOTE_DB;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
+import static org.eclipse.jgit.lib.ObjectId.zeroId;
 
 import com.google.auto.value.AutoValue;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Predicates;
 import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDbUtil;
 import com.google.gerrit.server.git.RefCache;
 
 import org.eclipse.jgit.lib.ObjectId;
 
 import java.io.IOException;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -48,13 +50,35 @@
  * Stored serialized in the {@code Change#noteDbState} field, and used to
  * determine whether the state in NoteDb is out of date.
  * <p>
- * Serialized in the form:
- * <pre>
- *   [meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]...
- * </pre>
+ * Serialized in one of the forms:
+ * <ul>
+ *    <li>[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]...
+ *    <li>R[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]...
+ *    <li>N
+ * </ul>
+ *
  * in numeric account ID order, with hex SHA-1s for human readability.
  */
 public class NoteDbChangeState {
+  public static final String NOTE_DB_PRIMARY_STATE = "N";
+
+  public enum PrimaryStorage {
+    REVIEW_DB('R', true),
+    NOTE_DB('N', false);
+
+    private final char code;
+    private final boolean writeToReviewDb;
+
+    private PrimaryStorage(char code, boolean writeToReviewDb) {
+      this.code = code;
+      this.writeToReviewDb = writeToReviewDb;
+    }
+
+    public boolean writeToReviewDb() {
+      return writeToReviewDb;
+    }
+  }
+
   @AutoValue
   public abstract static class Delta {
     static Delta create(Change.Id changeId, Optional<ObjectId> newChangeMetaId,
@@ -73,31 +97,89 @@
     abstract ImmutableMap<Account.Id, ObjectId> newDraftIds();
   }
 
+  @AutoValue
+  public abstract static class RefState {
+    @VisibleForTesting
+    public static RefState create(ObjectId changeMetaId,
+        Map<Account.Id, ObjectId> draftIds) {
+      return new AutoValue_NoteDbChangeState_RefState(
+          changeMetaId.copy(),
+          ImmutableMap.copyOf(
+              Maps.filterValues(draftIds, id -> !zeroId().equals(id))));
+    }
+
+    private static Optional<RefState> parse(Change.Id changeId,
+        List<String> parts) {
+      checkArgument(!parts.isEmpty(),
+          "missing state string for change %s", changeId);
+      ObjectId changeMetaId = ObjectId.fromString(parts.get(0));
+      Map<Account.Id, ObjectId> draftIds =
+          Maps.newHashMapWithExpectedSize(parts.size() - 1);
+      Splitter s = Splitter.on('=');
+      for (int i = 1; i < parts.size(); i++) {
+        String p = parts.get(i);
+        List<String> draftParts = s.splitToList(p);
+        checkArgument(draftParts.size() == 2,
+            "invalid draft state part for change %s: %s", changeId, p);
+        draftIds.put(Account.Id.parse(draftParts.get(0)),
+            ObjectId.fromString(draftParts.get(1)));
+      }
+      return Optional.of(create(changeMetaId, draftIds));
+    }
+
+    abstract ObjectId changeMetaId();
+    abstract ImmutableMap<Account.Id, ObjectId> draftIds();
+
+    @Override
+    public String toString() {
+      return appendTo(new StringBuilder()).toString();
+    }
+
+    StringBuilder appendTo(StringBuilder sb) {
+      sb.append(changeMetaId().name());
+      for (Account.Id id : ReviewDbUtil.intKeyOrdering()
+          .sortedCopy(draftIds().keySet())) {
+        sb.append(',')
+            .append(id.get())
+            .append('=')
+            .append(draftIds().get(id).name());
+      }
+      return sb;
+    }
+  }
+
   public static NoteDbChangeState parse(Change c) {
-    return parse(c.getId(), c.getNoteDbState());
+    return c != null ? parse(c.getId(), c.getNoteDbState()) : null;
   }
 
   @VisibleForTesting
   public static NoteDbChangeState parse(Change.Id id, String str) {
-    if (str == null) {
+    if (Strings.isNullOrEmpty(str)) {
+      // Return null rather than Optional as this is what goes in the field in
+      // ReviewDb.
       return null;
     }
     List<String> parts = Splitter.on(',').splitToList(str);
-    checkArgument(!parts.isEmpty(),
-        "invalid state string for change %s: %s", id, str);
-    ObjectId changeMetaId = ObjectId.fromString(parts.get(0));
-    Map<Account.Id, ObjectId> draftIds =
-        Maps.newHashMapWithExpectedSize(parts.size() - 1);
-    Splitter s = Splitter.on('=');
-    for (int i = 1; i < parts.size(); i++) {
-      String p = parts.get(i);
-      List<String> draftParts = s.splitToList(p);
-      checkArgument(draftParts.size() == 2,
-          "invalid draft state part for change %s: %s", id, p);
-      draftIds.put(Account.Id.parse(draftParts.get(0)),
-          ObjectId.fromString(draftParts.get(1)));
+
+    // Only valid NOTE_DB state is "N".
+    String first = parts.get(0);
+    if (parts.size() == 1 && first.charAt(0) == NOTE_DB.code) {
+      return new NoteDbChangeState(id, NOTE_DB, Optional.empty());
     }
-    return new NoteDbChangeState(id, changeMetaId, draftIds);
+
+    // Otherwise it must be REVIEW_DB, either "R,<RefState>" or just
+    // "<RefState>". Allow length > 0 for forward compatibility.
+    if (first.length() > 0) {
+      Optional<RefState> refState;
+      if (first.charAt(0) == REVIEW_DB.code) {
+        refState = RefState.parse(id, parts.subList(1, parts.size()));
+      } else {
+        refState = RefState.parse(id, parts);
+      }
+      return new NoteDbChangeState(id, REVIEW_DB, refState);
+    }
+    throw new IllegalArgumentException(
+        "invalid state string for change " + id + ": " + str);
   }
 
   public static NoteDbChangeState applyDelta(Change change, Delta delta) {
@@ -112,6 +194,10 @@
       return null;
     }
     NoteDbChangeState oldState = parse(change.getId(), oldStr);
+    if (oldState != null && oldState.getPrimaryStorage() == NOTE_DB) {
+      // NOTE_DB state doesn't include RefState, so applying a delta is a no-op.
+      return oldState;
+    }
 
     ObjectId changeMetaId;
     if (delta.newChangeMetaId().isPresent()) {
@@ -121,12 +207,12 @@
         return null;
       }
     } else {
-      changeMetaId = oldState.changeMetaId;
+      changeMetaId = oldState.getChangeMetaId();
     }
 
     Map<Account.Id, ObjectId> draftIds = new HashMap<>();
     if (oldState != null) {
-      draftIds.putAll(oldState.draftIds);
+      draftIds.putAll(oldState.getDraftIds());
     }
     for (Map.Entry<Account.Id, ObjectId> e : delta.newDraftIds().entrySet()) {
       if (e.getValue().equals(ObjectId.zeroId())) {
@@ -137,7 +223,11 @@
     }
 
     NoteDbChangeState state = new NoteDbChangeState(
-        change.getId(), changeMetaId, draftIds);
+        change.getId(),
+        oldState != null
+            ? oldState.getPrimaryStorage()
+            : REVIEW_DB,
+        Optional.of(RefState.create(changeMetaId, draftIds)));
     change.setNoteDbState(state.toString());
     return state;
   }
@@ -160,38 +250,47 @@
     return state.areDraftsUpToDate(draftsRepoRefs, accountId);
   }
 
-  public static String toString(ObjectId changeMetaId,
-      Map<Account.Id, ObjectId> draftIds) {
-    List<Account.Id> accountIds = Lists.newArrayList(draftIds.keySet());
-    Collections.sort(accountIds, comparing(Account.Id::get));
-    StringBuilder sb = new StringBuilder(changeMetaId.name());
-    for (Account.Id id : accountIds) {
-      sb.append(',')
-          .append(id.get())
-          .append('=')
-          .append(draftIds.get(id).name());
+  private final Change.Id changeId;
+  private final PrimaryStorage primaryStorage;
+  private final Optional<RefState> refState;
+
+  public NoteDbChangeState(
+      Change.Id changeId,
+      PrimaryStorage primaryStorage,
+      Optional<RefState> refState) {
+    this.changeId = checkNotNull(changeId);
+    this.primaryStorage = checkNotNull(primaryStorage);
+    this.refState = refState;
+
+    switch (primaryStorage) {
+      case REVIEW_DB:
+        checkArgument(
+            refState.isPresent(),
+            "expected RefState for change %s with primary storage %s",
+            changeId, primaryStorage);
+        break;
+      case NOTE_DB:
+        checkArgument(
+            !refState.isPresent(),
+            "expected no RefState for change %s with primary storage %s",
+            changeId, primaryStorage);
+        break;
+      default:
+        throw new IllegalStateException(
+            "invalid PrimaryStorage: " + primaryStorage);
     }
-    return sb.toString();
   }
 
-  private final Change.Id changeId;
-  private final ObjectId changeMetaId;
-  private final ImmutableMap<Account.Id, ObjectId> draftIds;
-
-  public NoteDbChangeState(Change.Id changeId, ObjectId changeMetaId,
-      Map<Account.Id, ObjectId> draftIds) {
-    this.changeId = checkNotNull(changeId);
-    this.changeMetaId = checkNotNull(changeMetaId);
-    this.draftIds = ImmutableMap.copyOf(Maps.filterValues(
-        draftIds, Predicates.not(Predicates.equalTo(ObjectId.zeroId()))));
+  public PrimaryStorage getPrimaryStorage() {
+    return primaryStorage;
   }
 
   public boolean isChangeUpToDate(RefCache changeRepoRefs) throws IOException {
     Optional<ObjectId> id = changeRepoRefs.get(changeMetaRef(changeId));
     if (!id.isPresent()) {
-      return changeMetaId.equals(ObjectId.zeroId());
+      return getChangeMetaId().equals(ObjectId.zeroId());
     }
-    return id.get().equals(changeMetaId);
+    return id.get().equals(getChangeMetaId());
   }
 
   public boolean areDraftsUpToDate(RefCache draftsRepoRefs, Account.Id accountId)
@@ -199,9 +298,9 @@
     Optional<ObjectId> id =
         draftsRepoRefs.get(refsDraftComments(changeId, accountId));
     if (!id.isPresent()) {
-      return !draftIds.containsKey(accountId);
+      return !getDraftIds().containsKey(accountId);
     }
-    return id.get().equals(draftIds.get(accountId));
+    return id.get().equals(getDraftIds().get(accountId));
   }
 
   public boolean isUpToDate(RefCache changeRepoRefs, RefCache draftsRepoRefs)
@@ -209,7 +308,7 @@
     if (!isChangeUpToDate(changeRepoRefs)) {
       return false;
     }
-    for (Account.Id accountId : draftIds.keySet()) {
+    for (Account.Id accountId : getDraftIds().keySet()) {
       if (!areDraftsUpToDate(draftsRepoRefs, accountId)) {
         return false;
       }
@@ -224,16 +323,36 @@
 
   @VisibleForTesting
   public ObjectId getChangeMetaId() {
-    return changeMetaId;
+    return refState().changeMetaId();
   }
 
   @VisibleForTesting
   ImmutableMap<Account.Id, ObjectId> getDraftIds() {
-    return draftIds;
+    return refState().draftIds();
+  }
+
+  @VisibleForTesting
+  Optional<RefState> getRefState() {
+    return refState;
+  }
+
+  private RefState refState() {
+    checkState(refState.isPresent(),
+        "state for %s has no RefState: %s", changeId, this);
+    return refState.get();
   }
 
   @Override
   public String toString() {
-    return toString(changeMetaId, draftIds);
+    switch (primaryStorage) {
+      case REVIEW_DB:
+        // Don't include enum field, just IDs (though parse would accept it).
+        return refState().toString();
+      case NOTE_DB:
+        return NOTE_DB_PRIMARY_STATE;
+      default:
+        throw new IllegalArgumentException(
+          "Unsupported PrimaryStorage: " + primaryStorage);
+    }
   }
 }
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 2e8cca7..b3aa420 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
@@ -54,6 +54,7 @@
 import com.google.gerrit.server.notedb.ChangeBundleReader;
 import com.google.gerrit.server.notedb.ChangeDraftUpdate;
 import com.google.gerrit.server.notedb.ChangeNoteUtil;
+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.NoteDbUpdateManager;
@@ -184,7 +185,7 @@
   public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId)
       throws NoSuchChangeException, IOException, OrmException {
     db = ReviewDbUtil.unwrapDb(db);
-    Change change = db.changes().get(changeId);
+    Change change = ChangeNotes.readOneReviewDbChange(db, changeId);
     if (change == null) {
       throw new NoSuchChangeException(changeId);
     }
@@ -200,7 +201,7 @@
       NoteDbUpdateManager manager) throws NoSuchChangeException, OrmException,
       IOException {
     db = ReviewDbUtil.unwrapDb(db);
-    Change change = db.changes().get(changeId);
+    Change change = ChangeNotes.readOneReviewDbChange(db, changeId);
     if (change == null) {
       throw new NoSuchChangeException(changeId);
     }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
index f2bf2be..e3613e3 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/notedb/NoteDbChangeStateTest.java
@@ -18,6 +18,8 @@
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.notedb.NoteDbChangeState.applyDelta;
 import static com.google.gerrit.server.notedb.NoteDbChangeState.parse;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.NOTE_DB;
+import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB;
 import static org.eclipse.jgit.lib.ObjectId.zeroId;
 
 import com.google.common.collect.ImmutableMap;
@@ -48,30 +50,44 @@
       ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee");
 
   @Test
-  public void parseWithoutDrafts() {
+  public void parseReviewDbWithoutDrafts() {
     NoteDbChangeState state = parse(new Change.Id(1), SHA1.name());
-
+    assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB);
     assertThat(state.getChangeId()).isEqualTo(new Change.Id(1));
     assertThat(state.getChangeMetaId()).isEqualTo(SHA1);
     assertThat(state.getDraftIds()).isEmpty();
+    assertThat(state.toString()).isEqualTo(SHA1.name());
 
+    state = parse(new Change.Id(1), "R," + SHA1.name());
+    assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB);
+    assertThat(state.getChangeId()).isEqualTo(new Change.Id(1));
+    assertThat(state.getChangeMetaId()).isEqualTo(SHA1);
+    assertThat(state.getDraftIds()).isEmpty();
     assertThat(state.toString()).isEqualTo(SHA1.name());
   }
 
   @Test
-  public void parseWithDrafts() {
-    NoteDbChangeState state = parse(
-        new Change.Id(1),
-        SHA1.name() + ",2003=" + SHA2.name() + ",1001=" + SHA3.name());
-
+  public void parseReviewDbWithDrafts() {
+    String str = SHA1.name() + ",2003=" + SHA2.name() + ",1001=" + SHA3.name();
+    String expected =
+        SHA1.name() + ",1001=" + SHA3.name() + ",2003=" + SHA2.name();
+    NoteDbChangeState state = parse(new Change.Id(1), str);
+    assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB);
     assertThat(state.getChangeId()).isEqualTo(new Change.Id(1));
     assertThat(state.getChangeMetaId()).isEqualTo(SHA1);
     assertThat(state.getDraftIds()).containsExactly(
         new Account.Id(1001), SHA3,
         new Account.Id(2003), SHA2);
+    assertThat(state.toString()).isEqualTo(expected);
 
-    assertThat(state.toString()).isEqualTo(
-        SHA1.name() + ",1001=" + SHA3.name() + ",2003=" + SHA2.name());
+    state = parse(new Change.Id(1), "R," + str);
+    assertThat(state.getPrimaryStorage()).isEqualTo(REVIEW_DB);
+    assertThat(state.getChangeId()).isEqualTo(new Change.Id(1));
+    assertThat(state.getChangeMetaId()).isEqualTo(SHA1);
+    assertThat(state.getDraftIds()).containsExactly(
+        new Account.Id(1001), SHA3,
+        new Account.Id(2003), SHA2);
+    assertThat(state.toString()).isEqualTo(expected);
   }
 
   @Test
@@ -127,6 +143,27 @@
         SHA3.name() + ",1001=" + SHA2.name());
   }
 
+  @Test
+  public void parseNoteDbPrimary() {
+    NoteDbChangeState state = parse(new Change.Id(1), "N");
+    assertThat(state.getPrimaryStorage()).isEqualTo(NOTE_DB);
+    assertThat(state.getRefState().isPresent()).isFalse();
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void parseInvalidPrimaryStorage() {
+    parse(new Change.Id(1), "X");
+  }
+
+  @Test
+  public void applyDeltaToNoteDbPrimaryIsNoOp() {
+    Change c = newChange();
+    c.setNoteDbState("N");
+    applyDelta(c, Delta.create(c.getId(), metaId(SHA1),
+        drafts(new Account.Id(1001), SHA2)));
+    assertThat(c.getNoteDbState()).isEqualTo("N");
+  }
+
   private static Change newChange() {
     return TestChanges.newChange(
         new Project.NameKey("project"), new Account.Id(12345));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
index f654f6a..893c8f2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetMembersCommand.java
@@ -19,6 +19,7 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -83,26 +84,30 @@
 
   @Override
   protected void run() throws UnloggedFailure, Failure, Exception {
-    for (AccountGroup.UUID groupUuid : groups) {
-      GroupResource resource =
-          groupsCollection.parse(TopLevelResource.INSTANCE,
-              IdString.fromUrl(groupUuid.get()));
-      if (!accountsToRemove.isEmpty()) {
-        deleteMembers.apply(resource, fromMembers(accountsToRemove));
-        reportMembersAction("removed from", resource, accountsToRemove);
+    try {
+      for (AccountGroup.UUID groupUuid : groups) {
+        GroupResource resource =
+            groupsCollection.parse(TopLevelResource.INSTANCE,
+                IdString.fromUrl(groupUuid.get()));
+        if (!accountsToRemove.isEmpty()) {
+          deleteMembers.apply(resource, fromMembers(accountsToRemove));
+          reportMembersAction("removed from", resource, accountsToRemove);
+        }
+        if (!groupsToRemove.isEmpty()) {
+          deleteIncludedGroups.apply(resource, fromGroups(groupsToRemove));
+          reportGroupsAction("excluded from", resource, groupsToRemove);
+        }
+        if (!accountsToAdd.isEmpty()) {
+          addMembers.apply(resource, fromMembers(accountsToAdd));
+          reportMembersAction("added to", resource, accountsToAdd);
+        }
+        if (!groupsToInclude.isEmpty()) {
+          addIncludedGroups.apply(resource, fromGroups(groupsToInclude));
+          reportGroupsAction("included to", resource, groupsToInclude);
+        }
       }
-      if (!groupsToRemove.isEmpty()) {
-        deleteIncludedGroups.apply(resource, fromGroups(groupsToRemove));
-        reportGroupsAction("excluded from", resource, groupsToRemove);
-      }
-      if (!accountsToAdd.isEmpty()) {
-        addMembers.apply(resource, fromMembers(accountsToAdd));
-        reportMembersAction("added to", resource, accountsToAdd);
-      }
-      if (!groupsToInclude.isEmpty()) {
-        addIncludedGroups.apply(resource, fromGroups(groupsToInclude));
-        reportGroupsAction("included to", resource, groupsToInclude);
-      }
+    } catch (RestApiException e) {
+      throw die(e.getMessage());
     }
   }
 
diff --git a/lib/auto/BUCK b/lib/auto/BUCK
index 6197e34..c186f87 100644
--- a/lib/auto/BUCK
+++ b/lib/auto/BUCK
@@ -2,8 +2,8 @@
 
 maven_jar(
   name = 'auto-value',
-  id = 'com.google.auto.value:auto-value:1.3-rc1',
-  sha1 = 'b764e0fb7e11353fbff493b22fd6e83bf091a179',
+  id = 'com.google.auto.value:auto-value:1.4-rc1',
+  sha1 = '9347939002003a7a3c3af48271fc2c18734528a4',
   license = 'Apache2.0',
   visibility = ['PUBLIC'],
 )
diff --git a/plugins/replication b/plugins/replication
index 3212bcd..e1beaa8 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 3212bcd4f2c0dc791a99af97ee98df70746f2306
+Subproject commit e1beaa8e16c05af5538d6459f9e4d3e4af500bca
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 38c48c5..6eeb6df 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
@@ -93,6 +93,10 @@
       .changeMetadata {
         font-size: .95em;
       }
+      /* Prevent plugin text from overflowing. */
+      #change_plugins {
+        word-break: break-all;
+      }
       .commitMessage {
         font-family: var(--monospace-font-family);
         flex: 0 0 72ch;
diff --git a/tools/bzl/gwt.bzl b/tools/bzl/gwt.bzl
index 27e0740..7eff506 100644
--- a/tools/bzl/gwt.bzl
+++ b/tools/bzl/gwt.bzl
@@ -231,7 +231,7 @@
       'unzip -qo $$ROOT/$(location :%s);' % opt +
       'mkdir -p $$(dirname $@);' +
       'zip -qr $$ROOT/$@ .',
-    out = 'ui_optdbg' + suffix + '.zip',
+    outs = ['ui_optdbg' + suffix + '.zip'],
     visibility = ['//visibility:public'],
    )
 
diff --git a/tools/maven/package.bzl b/tools/maven/package.bzl
index c996ac5..fbd08c6 100644
--- a/tools/maven/package.bzl
+++ b/tools/maven/package.bzl
@@ -13,10 +13,10 @@
 # limitations under the License.
 
 sh_bang_template = (' && '.join([
-  "echo '#!/bin/bash -eu' > $@",
+  "echo '#!/bin/bash -e' > $@",
   'echo "# this script should run from the root of your workspace." >> $@',
   'echo "" >> $@',
-  "echo 'if [[ -n \"$$$${VERBOSE:-}\" ]]; then set -x ; fi' >> $@",
+  "echo 'if [[ \"$$VERBOSE\" ]]; then set -x ; fi' >> $@",
   'echo "" >> $@',
   'echo %s >> $@',
   'echo "" >> $@',
diff --git a/tools/maven/package.defs b/tools/maven/package.defs
index c412ebd..a557170 100644
--- a/tools/maven/package.defs
+++ b/tools/maven/package.defs
@@ -13,10 +13,10 @@
 # limitations under the License.
 
 sh_bang_template = (' && '.join([
-  "echo '#!/bin/bash -eu' > $OUT",
+  "echo '#!/bin/bash -e' > $OUT",
   'echo "# this script should run from the root of your workspace." >> $OUT',
   'echo "" >> $OUT',
-  "echo 'if [[ -n \"$${VERBOSE:-}\" ]]; then set -x ; fi' >> $OUT",
+  "echo 'if [[ \"${VERBOSE}\" ]]; then set -x ; fi' >> $OUT",
   'echo "" >> $OUT',
   'echo %s >> $OUT',
   'echo "" >> $OUT',