Remove NoteDbChangeState
This was used for coordinating the state of migrating a single change to NoteDb, so is no longer required. Change-Id: Ie5c313140d810ded904d34273d6118fba2866516
This commit is contained in:
		| @@ -520,10 +520,6 @@ public final class Change { | |||||||
|   @Column(id = 23, notNull = false) |   @Column(id = 23, notNull = false) | ||||||
|   protected Id revertOf; |   protected Id revertOf; | ||||||
|  |  | ||||||
|   /** @see com.google.gerrit.server.notedb.NoteDbChangeState */ |  | ||||||
|   @Column(id = 101, notNull = false, length = Integer.MAX_VALUE) |  | ||||||
|   protected String noteDbState; |  | ||||||
|  |  | ||||||
|   protected Change() {} |   protected Change() {} | ||||||
|  |  | ||||||
|   public Change( |   public Change( | ||||||
| @@ -559,7 +555,6 @@ public final class Change { | |||||||
|     isPrivate = other.isPrivate; |     isPrivate = other.isPrivate; | ||||||
|     workInProgress = other.workInProgress; |     workInProgress = other.workInProgress; | ||||||
|     reviewStarted = other.reviewStarted; |     reviewStarted = other.reviewStarted; | ||||||
|     noteDbState = other.noteDbState; |  | ||||||
|     revertOf = other.revertOf; |     revertOf = other.revertOf; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -738,14 +733,6 @@ public final class Change { | |||||||
|     return this.revertOf; |     return this.revertOf; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public String getNoteDbState() { |  | ||||||
|     return noteDbState; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public void setNoteDbState(String state) { |  | ||||||
|     noteDbState = state; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @Override |   @Override | ||||||
|   public String toString() { |   public String toString() { | ||||||
|     return new StringBuilder(getClass().getSimpleName()) |     return new StringBuilder(getClass().getSimpleName()) | ||||||
|   | |||||||
| @@ -109,7 +109,6 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> { | |||||||
|  |  | ||||||
|     public ChangeNotes createChecked(Project.NameKey project, Change.Id changeId) |     public ChangeNotes createChecked(Project.NameKey project, Change.Id changeId) | ||||||
|         throws OrmException { |         throws OrmException { | ||||||
|       // Prepopulate the change exists with proper noteDbState field. |  | ||||||
|       Change change = newChange(project, changeId); |       Change change = newChange(project, changeId); | ||||||
|       return new ChangeNotes(args, change, true, null).load(); |       return new ChangeNotes(args, change, true, null).load(); | ||||||
|     } |     } | ||||||
| @@ -128,11 +127,8 @@ public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     public static Change newChange(Project.NameKey project, Change.Id changeId) { |     public static Change newChange(Project.NameKey project, Change.Id changeId) { | ||||||
|       Change change = |       return new Change( | ||||||
|           new Change( |           null, changeId, null, new Branch.NameKey(project, "INVALID_NOTE_DB_ONLY"), null); | ||||||
|               null, changeId, null, new Branch.NameKey(project, "INVALID_NOTE_DB_ONLY"), null); |  | ||||||
|       change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE); |  | ||||||
|       return change; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     public ChangeNotes create(Project.NameKey project, Change.Id changeId) throws OrmException { |     public ChangeNotes create(Project.NameKey project, Change.Id changeId) throws OrmException { | ||||||
|   | |||||||
| @@ -62,7 +62,6 @@ import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.Reviewer | |||||||
| import com.google.gerrit.server.cache.serialize.CacheSerializer; | import com.google.gerrit.server.cache.serialize.CacheSerializer; | ||||||
| import com.google.gerrit.server.cache.serialize.ObjectIdConverter; | import com.google.gerrit.server.cache.serialize.ObjectIdConverter; | ||||||
| import com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord; | import com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord; | ||||||
| import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; |  | ||||||
| import com.google.gson.Gson; | import com.google.gson.Gson; | ||||||
| import com.google.protobuf.ByteString; | import com.google.protobuf.ByteString; | ||||||
| import com.google.protobuf.MessageLite; | import com.google.protobuf.MessageLite; | ||||||
| @@ -173,9 +172,6 @@ public abstract class ChangeNotesState { | |||||||
|   /** |   /** | ||||||
|    * Subset of Change columns that can be represented in NoteDb. |    * Subset of Change columns that can be represented in NoteDb. | ||||||
|    * |    * | ||||||
|    * <p>Notable exceptions include rowVersion and noteDbState, which are only make sense when read |  | ||||||
|    * from NoteDb, so they cannot be cached. |  | ||||||
|    * |  | ||||||
|    * <p>Fields should match the column names in {@link Change}, and are in listed column order. |    * <p>Fields should match the column names in {@link Change}, and are in listed column order. | ||||||
|    */ |    */ | ||||||
|   @AutoValue |   @AutoValue | ||||||
| @@ -312,7 +308,6 @@ public abstract class ChangeNotesState { | |||||||
|             new Branch.NameKey(project, c.branch()), |             new Branch.NameKey(project, c.branch()), | ||||||
|             c.createdOn()); |             c.createdOn()); | ||||||
|     copyNonConstructorColumnsTo(change); |     copyNonConstructorColumnsTo(change); | ||||||
|     change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE); |  | ||||||
|     return change; |     return change; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -322,7 +317,6 @@ public abstract class ChangeNotesState { | |||||||
|         c != null && metaId() != null, |         c != null && metaId() != null, | ||||||
|         "missing columns or metaId in ChangeNotesState; is NoteDb enabled? %s", |         "missing columns or metaId in ChangeNotesState; is NoteDb enabled? %s", | ||||||
|         this); |         this); | ||||||
|     checkMetaId(change); |  | ||||||
|     change.setKey(c.changeKey()); |     change.setKey(c.changeKey()); | ||||||
|     change.setOwner(c.owner()); |     change.setOwner(c.owner()); | ||||||
|     change.setDest(new Branch.NameKey(change.getProject(), c.branch())); |     change.setDest(new Branch.NameKey(change.getProject(), c.branch())); | ||||||
| @@ -330,26 +324,6 @@ public abstract class ChangeNotesState { | |||||||
|     copyNonConstructorColumnsTo(change); |     copyNonConstructorColumnsTo(change); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private void checkMetaId(Change change) throws IOException { |  | ||||||
|     NoteDbChangeState state = NoteDbChangeState.parse(change); |  | ||||||
|     if (state == null) { |  | ||||||
|       return; // Can happen during small NoteDb tests. |  | ||||||
|     } else if (state.getPrimaryStorage() == PrimaryStorage.NOTE_DB) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     checkState(state.getRefState().isPresent(), "expected RefState: %s", state); |  | ||||||
|     ObjectId idFromState = state.getRefState().get().changeMetaId(); |  | ||||||
|     if (!idFromState.equals(metaId())) { |  | ||||||
|       throw new IOException( |  | ||||||
|           "cannot copy ChangeNotesState into Change " |  | ||||||
|               + changeId() |  | ||||||
|               + "; this ChangeNotesState was created from " |  | ||||||
|               + metaId() |  | ||||||
|               + ", but change requires state " |  | ||||||
|               + idFromState); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private void copyNonConstructorColumnsTo(Change change) { |   private void copyNonConstructorColumnsTo(Change change) { | ||||||
|     ChangeColumns c = requireNonNull(columns(), "columns are required"); |     ChangeColumns c = requireNonNull(columns(), "columns are required"); | ||||||
|     if (c.status() != null) { |     if (c.status() != null) { | ||||||
|   | |||||||
| @@ -1,441 +0,0 @@ | |||||||
| // Copyright (C) 2016 The Android Open Source Project |  | ||||||
| // |  | ||||||
| // Licensed under the Apache License, Version 2.0 (the "License"); |  | ||||||
| // you may not use this file except in compliance with the License. |  | ||||||
| // You may obtain a copy of the License at |  | ||||||
| // |  | ||||||
| // http://www.apache.org/licenses/LICENSE-2.0 |  | ||||||
| // |  | ||||||
| // Unless required by applicable law or agreed to in writing, software |  | ||||||
| // distributed under the License is distributed on an "AS IS" BASIS, |  | ||||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |  | ||||||
| // See the License for the specific language governing permissions and |  | ||||||
| // limitations under the License. |  | ||||||
|  |  | ||||||
| package com.google.gerrit.server.notedb; |  | ||||||
|  |  | ||||||
| import static com.google.common.base.Preconditions.checkArgument; |  | ||||||
| import static com.google.common.base.Preconditions.checkState; |  | ||||||
| import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef; |  | ||||||
| import static com.google.gerrit.reviewdb.client.RefNames.refsDraftComments; |  | ||||||
| import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.NOTE_DB; |  | ||||||
| import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; |  | ||||||
| import static java.util.Objects.requireNonNull; |  | ||||||
|  |  | ||||||
| import com.google.auto.value.AutoValue; |  | ||||||
| import com.google.common.annotations.VisibleForTesting; |  | ||||||
| import com.google.common.base.Splitter; |  | ||||||
| import com.google.common.base.Strings; |  | ||||||
| import com.google.common.collect.ImmutableMap; |  | ||||||
| import com.google.common.collect.Maps; |  | ||||||
| import com.google.common.primitives.Longs; |  | ||||||
| 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 com.google.gerrit.server.util.time.TimeUtil; |  | ||||||
| import com.google.gwtorm.server.OrmRuntimeException; |  | ||||||
| import java.io.IOException; |  | ||||||
| import java.sql.Timestamp; |  | ||||||
| import java.util.HashMap; |  | ||||||
| import java.util.List; |  | ||||||
| import java.util.Map; |  | ||||||
| import java.util.Objects; |  | ||||||
| import java.util.Optional; |  | ||||||
| import java.util.concurrent.TimeUnit; |  | ||||||
| import org.eclipse.jgit.lib.Config; |  | ||||||
| import org.eclipse.jgit.lib.ObjectId; |  | ||||||
|  |  | ||||||
| /** |  | ||||||
|  * The state of all relevant NoteDb refs across all repos corresponding to a given Change entity. |  | ||||||
|  * |  | ||||||
|  * <p>Stored serialized in the {@code Change#noteDbState} field, and used to determine whether the |  | ||||||
|  * state in NoteDb is out of date. |  | ||||||
|  * |  | ||||||
|  * <p>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>R=[read-only-until],[meta-sha],[account1]=[drafts-sha],[account2]=[drafts-sha]... |  | ||||||
|  *   <li>N |  | ||||||
|  *   <li>N=[read-only-until] |  | ||||||
|  * </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'), |  | ||||||
|     NOTE_DB('N'); |  | ||||||
|  |  | ||||||
|     private final char code; |  | ||||||
|  |  | ||||||
|     PrimaryStorage(char code) { |  | ||||||
|       this.code = code; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     public static PrimaryStorage of(@Nullable Change c) { |  | ||||||
|       NoteDbChangeState s = NoteDbChangeState.parse(c); |  | ||||||
|       return s != null ? s.getPrimaryStorage() : REVIEW_DB; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @AutoValue |  | ||||||
|   public abstract static class Delta { |  | ||||||
|     @VisibleForTesting |  | ||||||
|     public static Delta create( |  | ||||||
|         Change.Id changeId, |  | ||||||
|         Optional<ObjectId> newChangeMetaId, |  | ||||||
|         Map<Account.Id, ObjectId> newDraftIds) { |  | ||||||
|       if (newDraftIds == null) { |  | ||||||
|         newDraftIds = ImmutableMap.of(); |  | ||||||
|       } |  | ||||||
|       return new AutoValue_NoteDbChangeState_Delta( |  | ||||||
|           changeId, newChangeMetaId, ImmutableMap.copyOf(newDraftIds)); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     abstract Change.Id changeId(); |  | ||||||
|  |  | ||||||
|     abstract Optional<ObjectId> newChangeMetaId(); |  | ||||||
|  |  | ||||||
|     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 -> !ObjectId.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); |  | ||||||
|         Optional<Account.Id> accountId = Account.Id.tryParse(draftParts.get(0)); |  | ||||||
|         checkArgument( |  | ||||||
|             accountId.isPresent(), |  | ||||||
|             "invalid account ID in draft state part for change %s: %s", |  | ||||||
|             changeId, |  | ||||||
|             p); |  | ||||||
|         draftIds.put(accountId.get(), 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(@Nullable Change c) { |  | ||||||
|     return c != null ? parse(c.getId(), c.getNoteDbState()) : null; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @VisibleForTesting |  | ||||||
|   public static NoteDbChangeState parse(Change.Id id, @Nullable String str) { |  | ||||||
|     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); |  | ||||||
|     String first = parts.get(0); |  | ||||||
|     Optional<Timestamp> readOnlyUntil = parseReadOnlyUntil(id, str, first); |  | ||||||
|  |  | ||||||
|     // Only valid NOTE_DB state is "N". |  | ||||||
|     if (parts.size() == 1 && first.charAt(0) == NOTE_DB.code) { |  | ||||||
|       return new NoteDbChangeState(id, NOTE_DB, Optional.empty(), readOnlyUntil); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // 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, readOnlyUntil); |  | ||||||
|     } |  | ||||||
|     throw invalidState(id, str); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private static Optional<Timestamp> parseReadOnlyUntil( |  | ||||||
|       Change.Id id, String fullStr, String first) { |  | ||||||
|     if (first.length() > 2 && first.charAt(1) == '=') { |  | ||||||
|       Long ts = Longs.tryParse(first.substring(2)); |  | ||||||
|       if (ts == null) { |  | ||||||
|         throw invalidState(id, fullStr); |  | ||||||
|       } |  | ||||||
|       return Optional.of(new Timestamp(ts)); |  | ||||||
|     } |  | ||||||
|     return Optional.empty(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private static IllegalArgumentException invalidState(Change.Id id, String str) { |  | ||||||
|     return new IllegalArgumentException("invalid state string for change " + id + ": " + str); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   /** |  | ||||||
|    * Apply a delta to the state stored in a change entity. |  | ||||||
|    * |  | ||||||
|    * <p>This method does not check whether the old state was read-only; it is up to the caller to |  | ||||||
|    * not violate read-only semantics when storing the change back in ReviewDb. |  | ||||||
|    * |  | ||||||
|    * @param change change entity. The delta is applied against this entity's {@code noteDbState} and |  | ||||||
|    *     the new state is stored back in the entity as a side effect. |  | ||||||
|    * @param delta delta to apply. |  | ||||||
|    * @return new state, equivalent to what is stored in {@code change} as a side effect. |  | ||||||
|    */ |  | ||||||
|   public static NoteDbChangeState applyDelta(Change change, Delta delta) { |  | ||||||
|     if (delta == null) { |  | ||||||
|       return null; |  | ||||||
|     } |  | ||||||
|     String oldStr = change.getNoteDbState(); |  | ||||||
|     if (oldStr == null && !delta.newChangeMetaId().isPresent()) { |  | ||||||
|       // Neither an old nor a new meta ID was present, most likely because we |  | ||||||
|       // aren't writing a NoteDb graph at all for this change at this point. No |  | ||||||
|       // point in proceeding. |  | ||||||
|       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()) { |  | ||||||
|       changeMetaId = delta.newChangeMetaId().get(); |  | ||||||
|       if (changeMetaId.equals(ObjectId.zeroId())) { |  | ||||||
|         change.setNoteDbState(null); |  | ||||||
|         return null; |  | ||||||
|       } |  | ||||||
|     } else { |  | ||||||
|       changeMetaId = oldState.getChangeMetaId(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     Map<Account.Id, ObjectId> draftIds = new HashMap<>(); |  | ||||||
|     if (oldState != null) { |  | ||||||
|       draftIds.putAll(oldState.getDraftIds()); |  | ||||||
|     } |  | ||||||
|     for (Map.Entry<Account.Id, ObjectId> e : delta.newDraftIds().entrySet()) { |  | ||||||
|       if (e.getValue().equals(ObjectId.zeroId())) { |  | ||||||
|         draftIds.remove(e.getKey()); |  | ||||||
|       } else { |  | ||||||
|         draftIds.put(e.getKey(), e.getValue()); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     NoteDbChangeState state = |  | ||||||
|         new NoteDbChangeState( |  | ||||||
|             change.getId(), |  | ||||||
|             oldState != null ? oldState.getPrimaryStorage() : REVIEW_DB, |  | ||||||
|             Optional.of(RefState.create(changeMetaId, draftIds)), |  | ||||||
|             // Copy old read-only deadline rather than advancing it; the caller is |  | ||||||
|             // still responsible for finishing the rest of its work before the lease |  | ||||||
|             // runs out. |  | ||||||
|             oldState != null ? oldState.getReadOnlyUntil() : Optional.empty()); |  | ||||||
|     change.setNoteDbState(state.toString()); |  | ||||||
|     return state; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public static long getReadOnlySkew(Config cfg) { |  | ||||||
|     return cfg.getTimeUnit("notedb", null, "maxTimestampSkew", 1000, TimeUnit.MILLISECONDS); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static Timestamp timeForReadOnlyCheck(long skewMs) { |  | ||||||
|     // Subtract some slop in case the machine that set the change's read-only |  | ||||||
|     // lease has a clock behind ours. |  | ||||||
|     return new Timestamp(TimeUtil.nowMs() - skewMs); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public static void checkNotReadOnly(@Nullable Change change, long skewMs) { |  | ||||||
|     checkNotReadOnly(parse(change), skewMs); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public static void checkNotReadOnly(@Nullable NoteDbChangeState state, long skewMs) { |  | ||||||
|     if (state == null) { |  | ||||||
|       return; // No state means ReviewDb primary non-read-only. |  | ||||||
|     } else if (state.isReadOnly(timeForReadOnlyCheck(skewMs))) { |  | ||||||
|       throw new OrmRuntimeException( |  | ||||||
|           "change " |  | ||||||
|               + state.getChangeId() |  | ||||||
|               + " is read-only until " |  | ||||||
|               + state.getReadOnlyUntil().get()); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private final Change.Id changeId; |  | ||||||
|   private final PrimaryStorage primaryStorage; |  | ||||||
|   private final Optional<RefState> refState; |  | ||||||
|   private final Optional<Timestamp> readOnlyUntil; |  | ||||||
|  |  | ||||||
|   public NoteDbChangeState( |  | ||||||
|       Change.Id changeId, |  | ||||||
|       PrimaryStorage primaryStorage, |  | ||||||
|       Optional<RefState> refState, |  | ||||||
|       Optional<Timestamp> readOnlyUntil) { |  | ||||||
|     this.changeId = requireNonNull(changeId); |  | ||||||
|     this.primaryStorage = requireNonNull(primaryStorage); |  | ||||||
|     this.refState = requireNonNull(refState); |  | ||||||
|     this.readOnlyUntil = requireNonNull(readOnlyUntil); |  | ||||||
|  |  | ||||||
|     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); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public PrimaryStorage getPrimaryStorage() { |  | ||||||
|     return primaryStorage; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public boolean isChangeUpToDate(RefCache changeRepoRefs) throws IOException { |  | ||||||
|     if (primaryStorage == NOTE_DB) { |  | ||||||
|       return true; // Primary storage is NoteDb, up to date by definition. |  | ||||||
|     } |  | ||||||
|     Optional<ObjectId> id = changeRepoRefs.get(changeMetaRef(changeId)); |  | ||||||
|     if (!id.isPresent()) { |  | ||||||
|       return getChangeMetaId().equals(ObjectId.zeroId()); |  | ||||||
|     } |  | ||||||
|     return id.get().equals(getChangeMetaId()); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public boolean areDraftsUpToDate(RefCache draftsRepoRefs, Account.Id accountId) |  | ||||||
|       throws IOException { |  | ||||||
|     if (primaryStorage == NOTE_DB) { |  | ||||||
|       return true; // Primary storage is NoteDb, up to date by definition. |  | ||||||
|     } |  | ||||||
|     Optional<ObjectId> id = draftsRepoRefs.get(refsDraftComments(changeId, accountId)); |  | ||||||
|     if (!id.isPresent()) { |  | ||||||
|       return !getDraftIds().containsKey(accountId); |  | ||||||
|     } |  | ||||||
|     return id.get().equals(getDraftIds().get(accountId)); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public boolean isUpToDate(RefCache changeRepoRefs, RefCache draftsRepoRefs) throws IOException { |  | ||||||
|     if (primaryStorage == NOTE_DB) { |  | ||||||
|       return true; // Primary storage is NoteDb, up to date by definition. |  | ||||||
|     } |  | ||||||
|     if (!isChangeUpToDate(changeRepoRefs)) { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|     for (Account.Id accountId : getDraftIds().keySet()) { |  | ||||||
|       if (!areDraftsUpToDate(draftsRepoRefs, accountId)) { |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public boolean isReadOnly(Timestamp now) { |  | ||||||
|     return readOnlyUntil.isPresent() && now.before(readOnlyUntil.get()); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public Optional<Timestamp> getReadOnlyUntil() { |  | ||||||
|     return readOnlyUntil; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public NoteDbChangeState withReadOnlyUntil(Timestamp ts) { |  | ||||||
|     return new NoteDbChangeState(changeId, primaryStorage, refState, Optional.of(ts)); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public Change.Id getChangeId() { |  | ||||||
|     return changeId; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public ObjectId getChangeMetaId() { |  | ||||||
|     return refState().changeMetaId(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public ImmutableMap<Account.Id, ObjectId> getDraftIds() { |  | ||||||
|     return refState().draftIds(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public 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() { |  | ||||||
|     switch (primaryStorage) { |  | ||||||
|       case REVIEW_DB: |  | ||||||
|         if (!readOnlyUntil.isPresent()) { |  | ||||||
|           // Don't include enum field, just IDs (though parse would accept it). |  | ||||||
|           return refState().toString(); |  | ||||||
|         } |  | ||||||
|         return primaryStorage.code + "=" + readOnlyUntil.get().getTime() + "," + refState.get(); |  | ||||||
|       case NOTE_DB: |  | ||||||
|         if (!readOnlyUntil.isPresent()) { |  | ||||||
|           return NOTE_DB_PRIMARY_STATE; |  | ||||||
|         } |  | ||||||
|         return primaryStorage.code + "=" + readOnlyUntil.get().getTime(); |  | ||||||
|       default: |  | ||||||
|         throw new IllegalArgumentException("Unsupported PrimaryStorage: " + primaryStorage); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @Override |  | ||||||
|   public int hashCode() { |  | ||||||
|     return Objects.hash(changeId, primaryStorage, refState, readOnlyUntil); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @Override |  | ||||||
|   public boolean equals(Object o) { |  | ||||||
|     if (!(o instanceof NoteDbChangeState)) { |  | ||||||
|       return false; |  | ||||||
|     } |  | ||||||
|     NoteDbChangeState s = (NoteDbChangeState) o; |  | ||||||
|     return changeId.equals(s.changeId) |  | ||||||
|         && primaryStorage.equals(s.primaryStorage) |  | ||||||
|         && refState.equals(s.refState) |  | ||||||
|         && readOnlyUntil.equals(s.readOnlyUntil); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,241 +0,0 @@ | |||||||
| // Copyright (C) 2016 The Android Open Source Project |  | ||||||
| // |  | ||||||
| // Licensed under the Apache License, Version 2.0 (the "License"); |  | ||||||
| // you may not use this file except in compliance with the License. |  | ||||||
| // You may obtain a copy of the License at |  | ||||||
| // |  | ||||||
| // http://www.apache.org/licenses/LICENSE-2.0 |  | ||||||
| // |  | ||||||
| // Unless required by applicable law or agreed to in writing, software |  | ||||||
| // distributed under the License is distributed on an "AS IS" BASIS, |  | ||||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |  | ||||||
| // See the License for the specific language governing permissions and |  | ||||||
| // limitations under the License. |  | ||||||
|  |  | ||||||
| package com.google.gerrit.server.notedb; |  | ||||||
|  |  | ||||||
| import static com.google.common.base.Preconditions.checkArgument; |  | ||||||
| import static com.google.common.truth.Truth.assertThat; |  | ||||||
| import static com.google.common.truth.Truth8.assertThat; |  | ||||||
| import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.NOTE_DB; |  | ||||||
| import static com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage.REVIEW_DB; |  | ||||||
| import static com.google.gerrit.server.notedb.NoteDbChangeState.applyDelta; |  | ||||||
| import static com.google.gerrit.server.notedb.NoteDbChangeState.parse; |  | ||||||
| import static com.google.gerrit.server.util.time.TimeUtil.nowTs; |  | ||||||
| import static org.eclipse.jgit.lib.ObjectId.zeroId; |  | ||||||
|  |  | ||||||
| import com.google.common.collect.ImmutableMap; |  | ||||||
| import com.google.gerrit.reviewdb.client.Account; |  | ||||||
| import com.google.gerrit.reviewdb.client.Change; |  | ||||||
| import com.google.gerrit.reviewdb.client.Project; |  | ||||||
| import com.google.gerrit.server.notedb.NoteDbChangeState.Delta; |  | ||||||
| import com.google.gerrit.server.notedb.NoteDbChangeState.RefState; |  | ||||||
| import com.google.gerrit.testing.GerritBaseTests; |  | ||||||
| import com.google.gerrit.testing.TestChanges; |  | ||||||
| import com.google.gerrit.testing.TestTimeUtil; |  | ||||||
| import java.sql.Timestamp; |  | ||||||
| import java.util.Optional; |  | ||||||
| import java.util.concurrent.TimeUnit; |  | ||||||
| import org.eclipse.jgit.lib.ObjectId; |  | ||||||
| import org.junit.After; |  | ||||||
| import org.junit.Before; |  | ||||||
| import org.junit.Test; |  | ||||||
|  |  | ||||||
| /** Unit tests for {@link NoteDbChangeState}. */ |  | ||||||
| public class NoteDbChangeStateTest extends GerritBaseTests { |  | ||||||
|   ObjectId SHA1 = ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); |  | ||||||
|   ObjectId SHA2 = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"); |  | ||||||
|   ObjectId SHA3 = ObjectId.fromString("badc0feebadc0feebadc0feebadc0feebadc0fee"); |  | ||||||
|  |  | ||||||
|   @Before |  | ||||||
|   public void setUp() { |  | ||||||
|     TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @After |  | ||||||
|   public void tearDown() { |  | ||||||
|     TestTimeUtil.useSystemTime(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @Test |  | ||||||
|   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.getReadOnlyUntil()).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.getReadOnlyUntil()).isEmpty(); |  | ||||||
|     assertThat(state.toString()).isEqualTo(SHA1.name()); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @Test |  | ||||||
|   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.getReadOnlyUntil()).isEmpty(); |  | ||||||
|     assertThat(state.toString()).isEqualTo(expected); |  | ||||||
|  |  | ||||||
|     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.getReadOnlyUntil()).isEmpty(); |  | ||||||
|     assertThat(state.toString()).isEqualTo(expected); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @Test |  | ||||||
|   public void parseReadOnlyUntil() { |  | ||||||
|     Timestamp ts = new Timestamp(12345); |  | ||||||
|     String str = "R=12345," + SHA1.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.getReadOnlyUntil().get()).isEqualTo(ts); |  | ||||||
|     assertThat(state.toString()).isEqualTo(str); |  | ||||||
|  |  | ||||||
|     str = "N=12345"; |  | ||||||
|     state = parse(new Change.Id(1), str); |  | ||||||
|     assertThat(state.getPrimaryStorage()).isEqualTo(NOTE_DB); |  | ||||||
|     assertThat(state.getChangeId()).isEqualTo(new Change.Id(1)); |  | ||||||
|     assertThat(state.getRefState()).isEmpty(); |  | ||||||
|     assertThat(state.getReadOnlyUntil().get()).isEqualTo(ts); |  | ||||||
|     assertThat(state.toString()).isEqualTo(str); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @Test |  | ||||||
|   public void applyDeltaToNullWithNoNewMetaId() throws Exception { |  | ||||||
|     Change c = newChange(); |  | ||||||
|     assertThat(c.getNoteDbState()).isNull(); |  | ||||||
|     applyDelta(c, Delta.create(c.getId(), noMetaId(), noDrafts())); |  | ||||||
|     assertThat(c.getNoteDbState()).isNull(); |  | ||||||
|  |  | ||||||
|     applyDelta(c, Delta.create(c.getId(), noMetaId(), drafts(new Account.Id(1001), zeroId()))); |  | ||||||
|     assertThat(c.getNoteDbState()).isNull(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @Test |  | ||||||
|   public void applyDeltaToMetaId() throws Exception { |  | ||||||
|     Change c = newChange(); |  | ||||||
|     applyDelta(c, Delta.create(c.getId(), metaId(SHA1), noDrafts())); |  | ||||||
|     assertThat(c.getNoteDbState()).isEqualTo(SHA1.name()); |  | ||||||
|  |  | ||||||
|     applyDelta(c, Delta.create(c.getId(), metaId(SHA2), noDrafts())); |  | ||||||
|     assertThat(c.getNoteDbState()).isEqualTo(SHA2.name()); |  | ||||||
|  |  | ||||||
|     // No-op delta. |  | ||||||
|     applyDelta(c, Delta.create(c.getId(), noMetaId(), noDrafts())); |  | ||||||
|     assertThat(c.getNoteDbState()).isEqualTo(SHA2.name()); |  | ||||||
|  |  | ||||||
|     // Set to zero clears the field. |  | ||||||
|     applyDelta(c, Delta.create(c.getId(), metaId(zeroId()), noDrafts())); |  | ||||||
|     assertThat(c.getNoteDbState()).isNull(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @Test |  | ||||||
|   public void applyDeltaToDrafts() throws Exception { |  | ||||||
|     Change c = newChange(); |  | ||||||
|     applyDelta(c, Delta.create(c.getId(), metaId(SHA1), drafts(new Account.Id(1001), SHA2))); |  | ||||||
|     assertThat(c.getNoteDbState()).isEqualTo(SHA1.name() + ",1001=" + SHA2.name()); |  | ||||||
|  |  | ||||||
|     applyDelta(c, Delta.create(c.getId(), noMetaId(), drafts(new Account.Id(2003), SHA3))); |  | ||||||
|     assertThat(c.getNoteDbState()) |  | ||||||
|         .isEqualTo(SHA1.name() + ",1001=" + SHA2.name() + ",2003=" + SHA3.name()); |  | ||||||
|  |  | ||||||
|     applyDelta(c, Delta.create(c.getId(), noMetaId(), drafts(new Account.Id(2003), zeroId()))); |  | ||||||
|     assertThat(c.getNoteDbState()).isEqualTo(SHA1.name() + ",1001=" + SHA2.name()); |  | ||||||
|  |  | ||||||
|     applyDelta(c, Delta.create(c.getId(), metaId(SHA3), noDrafts())); |  | ||||||
|     assertThat(c.getNoteDbState()).isEqualTo(SHA3.name() + ",1001=" + SHA2.name()); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @Test |  | ||||||
|   public void applyDeltaToReadOnly() throws Exception { |  | ||||||
|     Timestamp ts = nowTs(); |  | ||||||
|     Change c = newChange(); |  | ||||||
|     NoteDbChangeState state = |  | ||||||
|         new NoteDbChangeState( |  | ||||||
|             c.getId(), |  | ||||||
|             REVIEW_DB, |  | ||||||
|             Optional.of(RefState.create(SHA1, ImmutableMap.of())), |  | ||||||
|             Optional.of(new Timestamp(ts.getTime() + 10000))); |  | ||||||
|     c.setNoteDbState(state.toString()); |  | ||||||
|     Delta delta = Delta.create(c.getId(), metaId(SHA2), noDrafts()); |  | ||||||
|     applyDelta(c, delta); |  | ||||||
|     assertThat(NoteDbChangeState.parse(c)) |  | ||||||
|         .isEqualTo( |  | ||||||
|             new NoteDbChangeState( |  | ||||||
|                 state.getChangeId(), |  | ||||||
|                 state.getPrimaryStorage(), |  | ||||||
|                 Optional.of(RefState.create(SHA2, ImmutableMap.of())), |  | ||||||
|                 state.getReadOnlyUntil())); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @Test |  | ||||||
|   public void parseNoteDbPrimary() { |  | ||||||
|     NoteDbChangeState state = parse(new Change.Id(1), "N"); |  | ||||||
|     assertThat(state.getPrimaryStorage()).isEqualTo(NOTE_DB); |  | ||||||
|     assertThat(state.getRefState()).isEmpty(); |  | ||||||
|     assertThat(state.getReadOnlyUntil()).isEmpty(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @Test(expected = IllegalArgumentException.class) |  | ||||||
|   public void parseInvalidPrimaryStorage() { |  | ||||||
|     parse(new Change.Id(1), "X"); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   @Test |  | ||||||
|   public void applyDeltaToNoteDbPrimaryIsNoOp() throws Exception { |  | ||||||
|     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)); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Static factory methods to avoid type arguments when using as method args. |  | ||||||
|  |  | ||||||
|   private static Optional<ObjectId> noMetaId() { |  | ||||||
|     return Optional.empty(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private static Optional<ObjectId> metaId(ObjectId id) { |  | ||||||
|     return Optional.of(id); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private static ImmutableMap<Account.Id, ObjectId> noDrafts() { |  | ||||||
|     return ImmutableMap.of(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private static ImmutableMap<Account.Id, ObjectId> drafts(Object... args) { |  | ||||||
|     checkArgument(args.length % 2 == 0); |  | ||||||
|     ImmutableMap.Builder<Account.Id, ObjectId> b = ImmutableMap.builder(); |  | ||||||
|     for (int i = 0; i < args.length / 2; i++) { |  | ||||||
|       b.put((Account.Id) args[2 * i], (ObjectId) args[2 * i + 1]); |  | ||||||
|     } |  | ||||||
|     return b.build(); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -51,14 +51,14 @@ message Change { | |||||||
|   optional bool work_in_progress = 21; |   optional bool work_in_progress = 21; | ||||||
|   optional bool review_started = 22; |   optional bool review_started = 22; | ||||||
|   optional Change_Id revert_of = 23; |   optional Change_Id revert_of = 23; | ||||||
|   optional string note_db_state = 101; |  | ||||||
|  |  | ||||||
|   // Deleted fields, should not be reused: |   // Deleted fields, should not be reused: | ||||||
|   reserved 6;  // sortkey |   reserved 6;    // sortkey | ||||||
|   reserved 9;  // open |   reserved 9;    // open | ||||||
|   reserved 11; // nbrPatchSets |   reserved 11;   // nbrPatchSets | ||||||
|   reserved 15; // lastSha1MergeTested |   reserved 15;   // lastSha1MergeTested | ||||||
|   reserved 16; // mergeable |   reserved 16;   // mergeable | ||||||
|  |   reserved 101;  // note_db_state | ||||||
| } | } | ||||||
|  |  | ||||||
| // Serialized form of com.google.gerrit.reviewdb.client.ChangeMessage. | // Serialized form of com.google.gerrit.reviewdb.client.ChangeMessage. | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Dave Borowitz
					Dave Borowitz