Merge "Add method for toggling dark theme"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index ce7adc2..7ed0e17 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -773,10 +773,11 @@
+
Default is 128 MiB per cache, except:
+
+* `"change_notes"`: disk storage is disabled by default
* `"diff_summary"`: default is `1g` (1 GiB of disk space)
+
-If 0, disk storage for the cache is disabled.
+If 0 or negative, disk storage for the cache is disabled.
==== [[cache_names]]Standard Caches
diff --git a/WORKSPACE b/WORKSPACE
index 15d8651..94138e4 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -711,6 +711,18 @@
sha1 = "636e49d675bc28e0b3ae0edd077d6acbbb159166",
)
+maven_jar(
+ name = "truth-liteproto-extension",
+ artifact = "com.google.truth.extensions:truth-liteproto-extension:" + TRUTH_VERS,
+ sha1 = "21210ac07e5cfbe83f04733f806224a6c0ae4d2d",
+)
+
+maven_jar(
+ name = "truth-proto-extension",
+ artifact = "com.google.truth.extensions:truth-proto-extension:" + TRUTH_VERS,
+ sha1 = "5a2b504143a5fec2b6be8bce292b3b7577a81789",
+)
+
# When bumping the easymock version number, make sure to also move powermock to a compatible version
maven_jar(
name = "easymock",
diff --git a/contrib/populate-fixture-data.py b/contrib/populate-fixture-data.py
index 93ac34f..e7e8d0b 100755
--- a/contrib/populate-fixture-data.py
+++ b/contrib/populate-fixture-data.py
@@ -160,7 +160,7 @@
def generate_random_text():
return " ".join([random.choice("lorem ipsum "
"doleret delendam "
- "\n esse".split(" ")) for _ in xrange(1, 100)])
+ "\n esse".split(" ")) for _ in range(1, 100)])
def set_up():
@@ -299,7 +299,7 @@
project_names = create_gerrit_projects(group_names)
for idx, u in enumerate(gerrit_users):
- for _ in xrange(random.randint(1, 5)):
+ for _ in range(random.randint(1, 5)):
create_change(u, project_names[4 * idx / len(gerrit_users)])
main()
diff --git a/java/com/google/gerrit/reviewdb/server/ReviewDbCodecs.java b/java/com/google/gerrit/reviewdb/server/ReviewDbCodecs.java
index 631e7f5..2958464 100644
--- a/java/com/google/gerrit/reviewdb/server/ReviewDbCodecs.java
+++ b/java/com/google/gerrit/reviewdb/server/ReviewDbCodecs.java
@@ -15,6 +15,7 @@
package com.google.gerrit.reviewdb.server;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gwtorm.protobuf.CodecFactory;
@@ -27,6 +28,9 @@
public static final ProtobufCodec<Change> CHANGE_CODEC = CodecFactory.encoder(Change.class);
+ public static final ProtobufCodec<ChangeMessage> MESSAGE_CODEC =
+ CodecFactory.encoder(ChangeMessage.class);
+
public static final ProtobufCodec<PatchSet> PATCH_SET_CODEC =
CodecFactory.encoder(PatchSet.class);
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 2f36cf2..996e602 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -106,7 +106,8 @@
* <li>binding {@link GitReferenceUpdated#DISABLED} and
* <li>passing an {@link
* com.google.gerrit.server.account.externalids.ExternalIdNotes.FactoryNoReindex} factory as
- * parameter of {@link AccountsUpdate.Factory#create(IdentifiedUser, ExternalIdNotesLoader)}
+ * parameter of {@link AccountsUpdate.Factory#create(IdentifiedUser,
+ * ExternalIdNotes.ExternalIdNotesLoader)}
* </ul>
*
* <p>If there are concurrent account updates updating the user branch in NoteDb may fail with
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheBinding.java b/java/com/google/gerrit/server/cache/PersistentCacheBinding.java
index 429f5ab..794d3bb 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheBinding.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheBinding.java
@@ -34,7 +34,12 @@
PersistentCacheBinding<K, V> version(int version);
- /** Set the total on-disk limit of the cache */
+ /**
+ * Set the total on-disk limit of the cache.
+ *
+ * <p>If 0 or negative, persistence for the cache is disabled by default, but may still be
+ * overridden in the config.
+ */
PersistentCacheBinding<K, V> diskLimit(long limit);
PersistentCacheBinding<K, V> keySerializer(CacheSerializer<K> keySerializer);
diff --git a/java/com/google/gerrit/server/cache/PersistentCacheProvider.java b/java/com/google/gerrit/server/cache/PersistentCacheProvider.java
index 405de4f..46a9e61 100644
--- a/java/com/google/gerrit/server/cache/PersistentCacheProvider.java
+++ b/java/com/google/gerrit/server/cache/PersistentCacheProvider.java
@@ -39,6 +39,7 @@
CacheModule module, String name, TypeLiteral<K> keyType, TypeLiteral<V> valType) {
super(module, name, keyType, valType);
version = -1;
+ diskLimit = 128 << 20;
}
@Inject(optional = true)
@@ -93,10 +94,7 @@
@Override
public long diskLimit() {
- if (diskLimit > 0) {
- return diskLimit;
- }
- return 128 << 20;
+ return diskLimit;
}
@Override
diff --git a/java/com/google/gerrit/server/cache/ProtoCacheSerializers.java b/java/com/google/gerrit/server/cache/ProtoCacheSerializers.java
index 795df72..9fe6b83 100644
--- a/java/com/google/gerrit/server/cache/ProtoCacheSerializers.java
+++ b/java/com/google/gerrit/server/cache/ProtoCacheSerializers.java
@@ -14,6 +14,8 @@
package com.google.gerrit.server.cache;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.protobuf.ByteString;
import com.google.protobuf.CodedOutputStream;
import com.google.protobuf.MessageLite;
import java.io.IOException;
@@ -23,8 +25,8 @@
/**
* Serializes a proto to a byte array.
*
- * <p>Guarantees deterministic serialization and thus is suitable for use as a persistent cache
- * key. Should be used in preference to {@link MessageLite#toByteArray()}, which is not guaranteed
+ * <p>Guarantees deterministic serialization and thus is suitable for use in persistent caches.
+ * Should be used in preference to {@link MessageLite#toByteArray()}, which is not guaranteed
* deterministic.
*
* @param message the proto message to serialize.
@@ -39,7 +41,30 @@
cout.checkNoSpaceLeft();
return bytes;
} catch (IOException e) {
- throw new IllegalStateException("exception writing to byte array");
+ throw new IllegalStateException("exception writing to byte array", e);
+ }
+ }
+
+ /**
+ * Serializes an object to a {@link ByteString} using a protobuf codec.
+ *
+ * <p>Guarantees deterministic serialization and thus is suitable for use in persistent caches.
+ * Should be used in preference to {@link ProtobufCodec#encodeToByteString(Object)}, which is not
+ * guaranteed deterministic.
+ *
+ * @param object the object to serialize.
+ * @param codec codec for serializing.
+ * @return a {@code ByteString} with the message contents.
+ */
+ public static <T> ByteString toByteString(T object, ProtobufCodec<T> codec) {
+ try (ByteString.Output bout = ByteString.newOutput()) {
+ CodedOutputStream cout = CodedOutputStream.newInstance(bout);
+ cout.useDeterministicSerialization();
+ codec.encode(object, cout);
+ cout.flush();
+ return bout.toByteString();
+ } catch (IOException e) {
+ throw new IllegalStateException("exception writing to ByteString", e);
}
}
diff --git a/java/com/google/gerrit/server/cache/testing/SerializedClassSubject.java b/java/com/google/gerrit/server/cache/testing/SerializedClassSubject.java
index 78900cb..19c5b67 100644
--- a/java/com/google/gerrit/server/cache/testing/SerializedClassSubject.java
+++ b/java/com/google/gerrit/server/cache/testing/SerializedClassSubject.java
@@ -22,8 +22,10 @@
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.Subject;
import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
+import java.util.Arrays;
import java.util.Map;
import org.apache.commons.lang3.reflect.FieldUtils;
@@ -62,6 +64,13 @@
super(metadata, actual);
}
+ public void isAbstract() {
+ isNotNull();
+ assertWithMessage("expected class %s to be abstract", actual().getName())
+ .that(Modifier.isAbstract(actual().getModifiers()))
+ .isTrue();
+ }
+
public void isConcrete() {
isNotNull();
assertWithMessage("expected class %s to be concrete", actual().getName())
@@ -78,4 +87,17 @@
.collect(toImmutableMap(Field::getName, Field::getGenericType)))
.containsExactlyEntriesIn(expectedFields);
}
+
+ public void hasAutoValueMethods(Map<String, Type> expectedMethods) {
+ // Would be nice if we could check clazz is an @AutoValue, but the retention is not RUNTIME.
+ isAbstract();
+ assertThat(
+ Arrays.stream(actual().getDeclaredMethods())
+ .filter(m -> !Modifier.isStatic(m.getModifiers()))
+ .filter(m -> Modifier.isAbstract(m.getModifiers()))
+ .filter(m -> m.getParameters().length == 0)
+ .collect(toImmutableMap(Method::getName, Method::getGenericReturnType)))
+ .named("no-argument abstract methods on %s", actual().getName())
+ .isEqualTo(expectedMethods);
+ }
}
diff --git a/java/com/google/gerrit/server/group/db/InternalGroupUpdate.java b/java/com/google/gerrit/server/group/db/InternalGroupUpdate.java
index 5ce3c1c..bff2952 100644
--- a/java/com/google/gerrit/server/group/db/InternalGroupUpdate.java
+++ b/java/com/google/gerrit/server/group/db/InternalGroupUpdate.java
@@ -142,8 +142,8 @@
* InternalGroupUpdate}.
*
* <p>This modification can be tweaked further and passed to {@link
- * #setMemberModification(MemberModification)} in order to combine multiple member additions,
- * deletions, or other modifications into one update.
+ * #setMemberModification(InternalGroupUpdate.MemberModification)} in order to combine multiple
+ * member additions, deletions, or other modifications into one update.
*/
public abstract MemberModification getMemberModification();
@@ -155,8 +155,8 @@
* InternalGroupUpdate}.
*
* <p>This modification can be tweaked further and passed to {@link
- * #setSubgroupModification(SubgroupModification)} in order to combine multiple subgroup
- * additions, deletions, or other modifications into one update.
+ * #setSubgroupModification(InternalGroupUpdate.SubgroupModification)} in order to combine
+ * multiple subgroup additions, deletions, or other modifications into one update.
*/
public abstract SubgroupModification getSubgroupModification();
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 5db347e..82253f2 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -643,7 +643,7 @@
* <p>Stored fields need to use a stable format over a long period; this type insulates the index
* from implementation changes in SubmitRecord itself.
*/
- static class StoredSubmitRecord {
+ public static class StoredSubmitRecord {
static class StoredLabel {
String label;
SubmitRecord.Label.Status status;
@@ -661,7 +661,7 @@
List<StoredRequirement> requirements;
String errorMessage;
- StoredSubmitRecord(SubmitRecord rec) {
+ public StoredSubmitRecord(SubmitRecord rec) {
this.status = rec.status;
this.errorMessage = rec.errorMessage;
if (rec.labels != null) {
@@ -686,7 +686,7 @@
}
}
- private SubmitRecord toSubmitRecord() {
+ public SubmitRecord toSubmitRecord() {
SubmitRecord rec = new SubmitRecord();
rec.status = status;
rec.errorMessage = errorMessage;
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
index 5658569..d1c28c4 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesCache.java
@@ -25,12 +25,16 @@
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.CacheSerializer;
+import com.google.gerrit.server.cache.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
import com.google.gerrit.server.notedb.AbstractChangeNotes.Args;
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
+import com.google.protobuf.ByteString;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@@ -38,6 +42,7 @@
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
@Singleton
@@ -49,20 +54,59 @@
@Override
protected void configure() {
bind(ChangeNotesCache.class);
- cache(CACHE_NAME, Key.class, ChangeNotesState.class)
+ persist(CACHE_NAME, Key.class, ChangeNotesState.class)
.weigher(Weigher.class)
- .maximumWeight(10 << 20);
+ .maximumWeight(10 << 20)
+ .diskLimit(-1)
+ .version(1)
+ .keySerializer(Key.Serializer.INSTANCE)
+ .valueSerializer(ChangeNotesState.Serializer.INSTANCE);
}
};
}
@AutoValue
public abstract static class Key {
+ static Key create(Project.NameKey project, Change.Id changeId, ObjectId id) {
+ return new AutoValue_ChangeNotesCache_Key(project, changeId, id.copy());
+ }
+
abstract Project.NameKey project();
abstract Change.Id changeId();
abstract ObjectId id();
+
+ @VisibleForTesting
+ static enum Serializer implements CacheSerializer<Key> {
+ INSTANCE;
+
+ @Override
+ public byte[] serialize(Key object) {
+ byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
+ object.id().copyRawTo(buf, 0);
+ return ProtoCacheSerializers.toByteArray(
+ ChangeNotesKeyProto.newBuilder()
+ .setProject(object.project().get())
+ .setChangeId(object.changeId().get())
+ .setId(ByteString.copyFrom(buf))
+ .build());
+ }
+
+ @Override
+ public Key deserialize(byte[] in) {
+ ChangeNotesKeyProto proto;
+ try {
+ proto = ChangeNotesKeyProto.parseFrom(in);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Failed to deserialize " + Key.class.getName());
+ }
+ return Key.create(
+ new Project.NameKey(proto.getProject()),
+ new Change.Id(proto.getChangeId()),
+ ObjectId.fromRaw(proto.getId().toByteArray()));
+ }
+ }
}
public static class Weigher implements com.google.common.cache.Weigher<Key, ChangeNotesState> {
@@ -134,7 +178,7 @@
+ T // readOnlyUntil
+ 1 // isPrivate
+ 1 // workInProgress
- + 1; // hasReviewStarted
+ + 1; // reviewStarted
}
private static int ptr(Object o, int size) {
@@ -330,7 +374,7 @@
Value get(Project.NameKey project, Change.Id changeId, ObjectId metaId, ChangeNotesRevWalk rw)
throws IOException {
try {
- Key key = new AutoValue_ChangeNotesCache_Key(project, changeId, metaId.copy());
+ Key key = Key.create(project, changeId, metaId);
Loader loader = new Loader(key, rw);
ChangeNotesState s = cache.get(key, loader);
return new AutoValue_ChangeNotesCache_Value(s, loader.revisionNoteMap);
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 78734f9..1b09494 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -14,15 +14,29 @@
package com.google.gerrit.server.notedb;
+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.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
+import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
+import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.MESSAGE_CODEC;
+import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
+import static com.google.gerrit.server.cache.ProtoCacheSerializers.toByteString;
import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Converter;
+import com.google.common.base.Enums;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableTable;
import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Table;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Account;
@@ -34,15 +48,28 @@
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
+import com.google.gerrit.server.cache.CacheSerializer;
+import com.google.gerrit.server.cache.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerStatusUpdateProto;
+import com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord;
+import com.google.gerrit.server.mail.Address;
import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage;
+import com.google.gson.Gson;
+import com.google.protobuf.ByteString;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
/**
@@ -95,7 +122,7 @@
@Nullable Timestamp readOnlyUntil,
boolean isPrivate,
boolean workInProgress,
- boolean hasReviewStarted,
+ boolean reviewStarted,
@Nullable Change.Id revertOf) {
checkNotNull(
metaId,
@@ -106,22 +133,22 @@
.metaId(metaId)
.changeId(changeId)
.columns(
- new AutoValue_ChangeNotesState_ChangeColumns.Builder()
+ ChangeColumns.builder()
.changeKey(changeKey)
.createdOn(createdOn)
.lastUpdatedOn(lastUpdatedOn)
.owner(owner)
.branch(branch)
+ .status(status)
.currentPatchSetId(currentPatchSetId)
.subject(subject)
.topic(topic)
.originalSubject(originalSubject)
.submissionId(submissionId)
.assignee(assignee)
- .status(status)
.isPrivate(isPrivate)
- .isWorkInProgress(workInProgress)
- .hasReviewStarted(hasReviewStarted)
+ .workInProgress(workInProgress)
+ .reviewStarted(reviewStarted)
.revertOf(revertOf)
.build())
.pastAssignees(pastAssignees)
@@ -147,10 +174,14 @@
* <p>Notable exceptions include rowVersion and noteDbState, which are only make sense when read
* from NoteDb, so they cannot be cached.
*
- * <p>Fields are in listed column order.
+ * <p>Fields should match the column names in {@link Change}, and are in listed column order.
*/
@AutoValue
abstract static class ChangeColumns {
+ static Builder builder() {
+ return new AutoValue_ChangeNotesState_ChangeColumns.Builder();
+ }
+
abstract Change.Key changeKey();
abstract Timestamp createdOn();
@@ -162,6 +193,10 @@
// Project not included, as it's not stored anywhere in the meta ref.
abstract String branch();
+ // TODO(dborowitz): Use a sensible default other than null
+ @Nullable
+ abstract Change.Status status();
+
@Nullable
abstract PatchSet.Id currentPatchSetId();
@@ -178,19 +213,18 @@
@Nullable
abstract Account.Id assignee();
- // TODO(dborowitz): Use a sensible default other than null
- @Nullable
- abstract Change.Status status();
abstract boolean isPrivate();
- abstract boolean isWorkInProgress();
+ abstract boolean workInProgress();
- abstract boolean hasReviewStarted();
+ abstract boolean reviewStarted();
@Nullable
abstract Change.Id revertOf();
+ abstract Builder toBuilder();
+
@AutoValue.Builder
abstract static class Builder {
abstract Builder changeKey(Change.Key changeKey);
@@ -219,9 +253,9 @@
abstract Builder isPrivate(boolean isPrivate);
- abstract Builder isWorkInProgress(boolean isWorkInProgress);
+ abstract Builder workInProgress(boolean workInProgress);
- abstract Builder hasReviewStarted(boolean hasReviewStarted);
+ abstract Builder reviewStarted(boolean reviewStarted);
abstract Builder revertOf(@Nullable Change.Id revertOf);
@@ -327,8 +361,8 @@
change.setSubmissionId(c.submissionId());
change.setAssignee(c.assignee());
change.setPrivate(c.isPrivate());
- change.setWorkInProgress(c.isWorkInProgress());
- change.setReviewStarted(c.hasReviewStarted());
+ change.setWorkInProgress(c.workInProgress());
+ change.setReviewStarted(c.reviewStarted());
change.setRevertOf(c.revertOf());
if (!patchSets().isEmpty()) {
@@ -368,7 +402,7 @@
abstract Builder pastAssignees(Set<Account.Id> pastAssignees);
- abstract Builder hashtags(Set<String> hashtags);
+ abstract Builder hashtags(Iterable<String> hashtags);
abstract Builder patchSets(Iterable<Map.Entry<PatchSet.Id, PatchSet>> patchSets);
@@ -396,4 +430,274 @@
abstract ChangeNotesState build();
}
+
+ static enum Serializer implements CacheSerializer<ChangeNotesState> {
+ INSTANCE;
+
+ @VisibleForTesting static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
+
+ private static final Converter<String, Change.Status> STATUS_CONVERTER =
+ Enums.stringConverter(Change.Status.class);
+ private static final Converter<String, ReviewerStateInternal> REVIEWER_STATE_CONVERTER =
+ Enums.stringConverter(ReviewerStateInternal.class);
+
+ @Override
+ public byte[] serialize(ChangeNotesState object) {
+ checkArgument(object.metaId() != null, "meta ID is required in: %s", object);
+ checkArgument(object.columns() != null, "ChangeColumns is required in: %s", object);
+ ChangeNotesStateProto.Builder b = ChangeNotesStateProto.newBuilder();
+
+ byte[] idBuf = new byte[Constants.OBJECT_ID_LENGTH];
+ object.metaId().copyRawTo(idBuf, 0);
+ b.setMetaId(ByteString.copyFrom(idBuf))
+ .setChangeId(object.changeId().get())
+ .setColumns(toChangeColumnsProto(object.columns()));
+
+ object.pastAssignees().forEach(a -> b.addPastAssignee(a.get()));
+ object.hashtags().forEach(b::addHashtag);
+ object.patchSets().forEach(e -> b.addPatchSet(toByteString(e.getValue(), PATCH_SET_CODEC)));
+ object.approvals().forEach(e -> b.addApproval(toByteString(e.getValue(), APPROVAL_CODEC)));
+
+ object.reviewers().asTable().cellSet().forEach(c -> b.addReviewer(toReviewerSetEntry(c)));
+ object
+ .reviewersByEmail()
+ .asTable()
+ .cellSet()
+ .forEach(c -> b.addReviewerByEmail(toReviewerByEmailSetEntry(c)));
+ object
+ .pendingReviewers()
+ .asTable()
+ .cellSet()
+ .forEach(c -> b.addPendingReviewer(toReviewerSetEntry(c)));
+ object
+ .pendingReviewersByEmail()
+ .asTable()
+ .cellSet()
+ .forEach(c -> b.addPendingReviewerByEmail(toReviewerByEmailSetEntry(c)));
+
+ object.allPastReviewers().forEach(a -> b.addPastReviewer(a.get()));
+ object.reviewerUpdates().forEach(u -> b.addReviewerUpdate(toReviewerStatusUpdateProto(u)));
+ object
+ .submitRecords()
+ .forEach(r -> b.addSubmitRecord(GSON.toJson(new StoredSubmitRecord(r))));
+ object.changeMessages().forEach(m -> b.addChangeMessage(toByteString(m, MESSAGE_CODEC)));
+ object.publishedComments().values().forEach(c -> b.addPublishedComment(GSON.toJson(c)));
+
+ if (object.readOnlyUntil() != null) {
+ b.setReadOnlyUntil(object.readOnlyUntil().getTime()).setHasReadOnlyUntil(true);
+ }
+
+ return ProtoCacheSerializers.toByteArray(b.build());
+ }
+
+ private static ChangeColumnsProto toChangeColumnsProto(ChangeColumns cols) {
+ ChangeColumnsProto.Builder b =
+ ChangeColumnsProto.newBuilder()
+ .setChangeKey(cols.changeKey().get())
+ .setCreatedOn(cols.createdOn().getTime())
+ .setLastUpdatedOn(cols.lastUpdatedOn().getTime())
+ .setOwner(cols.owner().get())
+ .setBranch(cols.branch());
+ if (cols.currentPatchSetId() != null) {
+ b.setCurrentPatchSetId(cols.currentPatchSetId().get()).setHasCurrentPatchSetId(true);
+ }
+ b.setSubject(cols.subject());
+ if (cols.topic() != null) {
+ b.setTopic(cols.topic()).setHasTopic(true);
+ }
+ if (cols.originalSubject() != null) {
+ b.setOriginalSubject(cols.originalSubject()).setHasOriginalSubject(true);
+ }
+ if (cols.submissionId() != null) {
+ b.setSubmissionId(cols.submissionId()).setHasSubmissionId(true);
+ }
+ if (cols.assignee() != null) {
+ b.setAssignee(cols.assignee().get()).setHasAssignee(true);
+ }
+ if (cols.status() != null) {
+ b.setStatus(STATUS_CONVERTER.reverse().convert(cols.status())).setHasStatus(true);
+ }
+ b.setIsPrivate(cols.isPrivate())
+ .setWorkInProgress(cols.workInProgress())
+ .setReviewStarted(cols.reviewStarted());
+ if (cols.revertOf() != null) {
+ b.setRevertOf(cols.revertOf().get()).setHasRevertOf(true);
+ }
+ return b.build();
+ }
+
+ private static ReviewerSetEntryProto toReviewerSetEntry(
+ Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> c) {
+ return ReviewerSetEntryProto.newBuilder()
+ .setState(REVIEWER_STATE_CONVERTER.reverse().convert(c.getRowKey()))
+ .setAccountId(c.getColumnKey().get())
+ .setTimestamp(c.getValue().getTime())
+ .build();
+ }
+
+ private static ReviewerByEmailSetEntryProto toReviewerByEmailSetEntry(
+ Table.Cell<ReviewerStateInternal, Address, Timestamp> c) {
+ return ReviewerByEmailSetEntryProto.newBuilder()
+ .setState(REVIEWER_STATE_CONVERTER.reverse().convert(c.getRowKey()))
+ .setAddress(c.getColumnKey().toHeaderString())
+ .setTimestamp(c.getValue().getTime())
+ .build();
+ }
+
+ private static ReviewerStatusUpdateProto toReviewerStatusUpdateProto(ReviewerStatusUpdate u) {
+ return ReviewerStatusUpdateProto.newBuilder()
+ .setDate(u.date().getTime())
+ .setUpdatedBy(u.updatedBy().get())
+ .setReviewer(u.reviewer().get())
+ .setState(REVIEWER_STATE_CONVERTER.reverse().convert(u.state()))
+ .build();
+ }
+
+ @Override
+ public ChangeNotesState deserialize(byte[] in) {
+ ChangeNotesStateProto proto;
+ try {
+ proto = ChangeNotesStateProto.parseFrom(in);
+ } catch (IOException e) {
+ throw new IllegalArgumentException(
+ "Failed to deserialize " + ChangeNotesState.class.getName());
+ }
+ Change.Id changeId = new Change.Id(proto.getChangeId());
+
+ ChangeNotesState.Builder b =
+ builder()
+ .metaId(ObjectId.fromRaw(proto.getMetaId().toByteArray()))
+ .changeId(changeId)
+ .columns(toChangeColumns(changeId, proto.getColumns()))
+ .pastAssignees(
+ proto
+ .getPastAssigneeList()
+ .stream()
+ .map(Account.Id::new)
+ .collect(toImmutableSet()))
+ .hashtags(proto.getHashtagList())
+ .patchSets(
+ proto
+ .getPatchSetList()
+ .stream()
+ .map(PATCH_SET_CODEC::decode)
+ .map(ps -> Maps.immutableEntry(ps.getId(), ps))
+ .collect(toImmutableList()))
+ .approvals(
+ proto
+ .getApprovalList()
+ .stream()
+ .map(APPROVAL_CODEC::decode)
+ .map(a -> Maps.immutableEntry(a.getPatchSetId(), a))
+ .collect(toImmutableList()))
+ .reviewers(toReviewerSet(proto.getReviewerList()))
+ .reviewersByEmail(toReviewerByEmailSet(proto.getReviewerByEmailList()))
+ .pendingReviewers(toReviewerSet(proto.getPendingReviewerList()))
+ .pendingReviewersByEmail(toReviewerByEmailSet(proto.getPendingReviewerByEmailList()))
+ .allPastReviewers(
+ proto
+ .getPastReviewerList()
+ .stream()
+ .map(Account.Id::new)
+ .collect(toImmutableList()))
+ .reviewerUpdates(toReviewerStatusUpdateList(proto.getReviewerUpdateList()))
+ .submitRecords(
+ proto
+ .getSubmitRecordList()
+ .stream()
+ .map(r -> GSON.fromJson(r, StoredSubmitRecord.class).toSubmitRecord())
+ .collect(toImmutableList()))
+ .changeMessages(
+ proto
+ .getChangeMessageList()
+ .stream()
+ .map(MESSAGE_CODEC::decode)
+ .collect(toImmutableList()))
+ .publishedComments(
+ proto
+ .getPublishedCommentList()
+ .stream()
+ .map(r -> GSON.fromJson(r, Comment.class))
+ .collect(toImmutableListMultimap(c -> new RevId(c.revId), c -> c)));
+ if (proto.getHasReadOnlyUntil()) {
+ b.readOnlyUntil(new Timestamp(proto.getReadOnlyUntil()));
+ }
+ return b.build();
+ }
+
+ private static ChangeColumns toChangeColumns(Change.Id changeId, ChangeColumnsProto proto) {
+ ChangeColumns.Builder b =
+ ChangeColumns.builder()
+ .changeKey(new Change.Key(proto.getChangeKey()))
+ .createdOn(new Timestamp(proto.getCreatedOn()))
+ .lastUpdatedOn(new Timestamp(proto.getLastUpdatedOn()))
+ .owner(new Account.Id(proto.getOwner()))
+ .branch(proto.getBranch());
+ if (proto.getHasCurrentPatchSetId()) {
+ b.currentPatchSetId(new PatchSet.Id(changeId, proto.getCurrentPatchSetId()));
+ }
+ b.subject(proto.getSubject());
+ if (proto.getHasTopic()) {
+ b.topic(proto.getTopic());
+ }
+ if (proto.getHasOriginalSubject()) {
+ b.originalSubject(proto.getOriginalSubject());
+ }
+ if (proto.getHasSubmissionId()) {
+ b.submissionId(proto.getSubmissionId());
+ }
+ if (proto.getHasAssignee()) {
+ b.assignee(new Account.Id(proto.getAssignee()));
+ }
+ if (proto.getHasStatus()) {
+ b.status(STATUS_CONVERTER.convert(proto.getStatus()));
+ }
+ b.isPrivate(proto.getIsPrivate())
+ .workInProgress(proto.getWorkInProgress())
+ .reviewStarted(proto.getReviewStarted());
+ if (proto.getHasRevertOf()) {
+ b.revertOf(new Change.Id(proto.getRevertOf()));
+ }
+ return b.build();
+ }
+
+ private static ReviewerSet toReviewerSet(List<ReviewerSetEntryProto> protos) {
+ ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Timestamp> b =
+ ImmutableTable.builder();
+ for (ReviewerSetEntryProto e : protos) {
+ b.put(
+ REVIEWER_STATE_CONVERTER.convert(e.getState()),
+ new Account.Id(e.getAccountId()),
+ new Timestamp(e.getTimestamp()));
+ }
+ return ReviewerSet.fromTable(b.build());
+ }
+
+ private static ReviewerByEmailSet toReviewerByEmailSet(
+ List<ReviewerByEmailSetEntryProto> protos) {
+ ImmutableTable.Builder<ReviewerStateInternal, Address, Timestamp> b =
+ ImmutableTable.builder();
+ for (ReviewerByEmailSetEntryProto e : protos) {
+ b.put(
+ REVIEWER_STATE_CONVERTER.convert(e.getState()),
+ Address.parse(e.getAddress()),
+ new Timestamp(e.getTimestamp()));
+ }
+ return ReviewerByEmailSet.fromTable(b.build());
+ }
+
+ private static ImmutableList<ReviewerStatusUpdate> toReviewerStatusUpdateList(
+ List<ReviewerStatusUpdateProto> protos) {
+ ImmutableList.Builder<ReviewerStatusUpdate> b = ImmutableList.builder();
+ for (ReviewerStatusUpdateProto proto : protos) {
+ b.add(
+ ReviewerStatusUpdate.create(
+ new Timestamp(proto.getDate()),
+ new Account.Id(proto.getUpdatedBy()),
+ new Account.Id(proto.getReviewer()),
+ REVIEWER_STATE_CONVERTER.convert(proto.getState())));
+ }
+ return b.build();
+ }
+ }
}
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index 1b11dd65..3113a8a 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -65,6 +65,7 @@
"//lib/jgit/org.eclipse.jgit.junit:junit",
"//lib/truth",
"//lib/truth:truth-java8-extension",
+ "//lib/truth:truth-proto-extension",
"//proto:cache_java_proto",
],
)
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java
new file mode 100644
index 0000000..5a7d812
--- /dev/null
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesCacheTest.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2018 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.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.server.cache.testing.CacheSerializerTestUtil.bytes;
+import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesKeyProto;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Test;
+
+public final class ChangeNotesCacheTest {
+ @Test
+ public void keySerializer() throws Exception {
+ ChangeNotesCache.Key key =
+ ChangeNotesCache.Key.create(
+ new Project.NameKey("project"),
+ new Change.Id(1234),
+ ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+ byte[] serialized = ChangeNotesCache.Key.Serializer.INSTANCE.serialize(key);
+ assertThat(ChangeNotesKeyProto.parseFrom(serialized))
+ .isEqualTo(
+ ChangeNotesKeyProto.newBuilder()
+ .setProject("project")
+ .setChangeId(1234)
+ .setId(
+ bytes(
+ 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef,
+ 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef))
+ .build());
+ assertThat(ChangeNotesCache.Key.Serializer.INSTANCE.deserialize(serialized)).isEqualTo(key);
+ }
+
+ @Test
+ public void keyMethods() throws Exception {
+ assertThatSerializedClass(ChangeNotesCache.Key.class)
+ .hasAutoValueMethods(
+ ImmutableMap.of(
+ "project", Project.NameKey.class,
+ "changeId", Change.Id.class,
+ "id", ObjectId.class));
+ }
+}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index d974877..b8f544a 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -442,17 +442,17 @@
// Change created in WIP remains in WIP.
RevCommit commit = writeCommit("Update WIP change\n" + "\n" + "Patch-set: 1\n", true);
ChangeNotesState state = newParser(commit).parseAll();
- assertThat(state.columns().hasReviewStarted()).isFalse();
+ assertThat(state.columns().reviewStarted()).isFalse();
// Moving change out of WIP starts review.
commit =
writeCommit("New ready change\n" + "\n" + "Patch-set: 1\n" + "Work-in-progress: false\n");
state = newParser(commit).parseAll();
- assertThat(state.columns().hasReviewStarted()).isTrue();
+ assertThat(state.columns().reviewStarted()).isTrue();
// Change created not in WIP has always been in review started state.
state = assertParseSucceeds("New change that doesn't declare WIP\n" + "\n" + "Patch-set: 1\n");
- assertThat(state.columns().hasReviewStarted()).isTrue();
+ assertThat(state.columns().reviewStarted()).isTrue();
}
@Test
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
new file mode 100644
index 0000000..c0f2c43
--- /dev/null
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -0,0 +1,957 @@
+// Copyright (C) 2018 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.truth.Truth.assertThat;
+import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
+import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.APPROVAL_CODEC;
+import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.MESSAGE_CODEC;
+import static com.google.gerrit.reviewdb.server.ReviewDbCodecs.PATCH_SET_CODEC;
+import static com.google.gerrit.server.cache.testing.SerializedClassSubject.assertThatSerializedClass;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableTable;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitRequirement;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Comment;
+import com.google.gerrit.reviewdb.client.LabelId;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.server.ReviewerByEmailSet;
+import com.google.gerrit.server.ReviewerSet;
+import com.google.gerrit.server.ReviewerStatusUpdate;
+import com.google.gerrit.server.cache.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ChangeColumnsProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerByEmailSetEntryProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerSetEntryProto;
+import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.ReviewerStatusUpdateProto;
+import com.google.gerrit.server.mail.Address;
+import com.google.gerrit.server.notedb.ChangeNotesState.ChangeColumns;
+import com.google.gerrit.server.notedb.ChangeNotesState.Serializer;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.protobuf.ProtobufCodec;
+import com.google.gwtorm.server.StandardKeyEncoder;
+import com.google.inject.TypeLiteral;
+import com.google.protobuf.ByteString;
+import java.lang.reflect.Type;
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ChangeNotesStateTest {
+ static {
+ KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+ }
+
+ private static final Change.Id ID = new Change.Id(123);
+ private static final ObjectId SHA =
+ ObjectId.fromString("1234567812345678123456781234567812345678");
+ private static final ByteString SHA_BYTES = toByteString(SHA);
+ private static final String CHANGE_KEY = "Iabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd";
+
+ private ChangeColumns cols;
+ private ChangeColumnsProto colsProto;
+
+ @Before
+ public void setUp() throws Exception {
+ cols =
+ ChangeColumns.builder()
+ .changeKey(new Change.Key(CHANGE_KEY))
+ .createdOn(new Timestamp(123456L))
+ .lastUpdatedOn(new Timestamp(234567L))
+ .owner(new Account.Id(1000))
+ .branch("refs/heads/master")
+ .subject("Test change")
+ .isPrivate(false)
+ .workInProgress(false)
+ .reviewStarted(true)
+ .build();
+ colsProto = toProto(newBuilder().build()).getColumns();
+ }
+
+ private ChangeNotesState.Builder newBuilder() {
+ return ChangeNotesState.Builder.empty(ID).metaId(SHA).columns(cols);
+ }
+
+ @Test
+ public void serializeChangeKey() throws Exception {
+ assertRoundTrip(
+ newBuilder()
+ .columns(
+ cols.toBuilder()
+ .changeKey(new Change.Key("Ieeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"))
+ .build())
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(
+ colsProto.toBuilder().setChangeKey("Ieeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"))
+ .build());
+ }
+
+ @Test
+ public void serializeCreatedOn() throws Exception {
+ assertRoundTrip(
+ newBuilder().columns(cols.toBuilder().createdOn(new Timestamp(98765L)).build()).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto.toBuilder().setCreatedOn(98765L))
+ .build());
+ }
+
+ @Test
+ public void serializeLastUpdatedOn() throws Exception {
+ assertRoundTrip(
+ newBuilder().columns(cols.toBuilder().lastUpdatedOn(new Timestamp(98765L)).build()).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto.toBuilder().setLastUpdatedOn(98765L))
+ .build());
+ }
+
+ @Test
+ public void serializeOwner() throws Exception {
+ assertRoundTrip(
+ newBuilder().columns(cols.toBuilder().owner(new Account.Id(7777)).build()).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto.toBuilder().setOwner(7777))
+ .build());
+ }
+
+ @Test
+ public void serializeBranch() throws Exception {
+ assertRoundTrip(
+ newBuilder().columns(cols.toBuilder().branch("refs/heads/bar").build()).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto.toBuilder().setBranch("refs/heads/bar"))
+ .build());
+ }
+
+ @Test
+ public void serializeSubject() throws Exception {
+ assertRoundTrip(
+ newBuilder().columns(cols.toBuilder().subject("A different test change").build()).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto.toBuilder().setSubject("A different test change"))
+ .build());
+ }
+
+ @Test
+ public void serializeCurrentPatchSetId() throws Exception {
+ assertRoundTrip(
+ newBuilder()
+ .columns(cols.toBuilder().currentPatchSetId(new PatchSet.Id(ID, 2)).build())
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto.toBuilder().setCurrentPatchSetId(2).setHasCurrentPatchSetId(true))
+ .build());
+ }
+
+ @Test
+ public void serializeNullTopic() throws Exception {
+ assertRoundTrip(
+ newBuilder().columns(cols.toBuilder().topic(null).build()).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .build());
+ }
+
+ @Test
+ public void serializeEmptyTopic() throws Exception {
+ ChangeNotesState state = newBuilder().columns(cols.toBuilder().topic("").build()).build();
+ assertRoundTrip(
+ state,
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto.toBuilder().setTopic("").setHasTopic(true))
+ .build());
+ }
+
+ @Test
+ public void serializeNonEmptyTopic() throws Exception {
+ ChangeNotesState state = newBuilder().columns(cols.toBuilder().topic("topic").build()).build();
+ assertRoundTrip(
+ state,
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto.toBuilder().setTopic("topic").setHasTopic(true))
+ .build());
+ }
+
+ @Test
+ public void serializeOriginalSubject() throws Exception {
+ assertRoundTrip(
+ newBuilder()
+ .columns(cols.toBuilder().originalSubject("The first patch set").build())
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(
+ colsProto
+ .toBuilder()
+ .setOriginalSubject("The first patch set")
+ .setHasOriginalSubject(true))
+ .build());
+ }
+
+ @Test
+ public void serializeSubmissionId() throws Exception {
+ assertRoundTrip(
+ newBuilder().columns(cols.toBuilder().submissionId("xyz").build()).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto.toBuilder().setSubmissionId("xyz").setHasSubmissionId(true))
+ .build());
+ }
+
+ @Test
+ public void serializeAssignee() throws Exception {
+ assertRoundTrip(
+ newBuilder().columns(cols.toBuilder().assignee(new Account.Id(2000)).build()).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto.toBuilder().setAssignee(2000).setHasAssignee(true))
+ .build());
+ }
+
+ @Test
+ public void serializeStatus() throws Exception {
+ assertRoundTrip(
+ newBuilder().columns(cols.toBuilder().status(Change.Status.MERGED).build()).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto.toBuilder().setStatus("MERGED").setHasStatus(true))
+ .build());
+ }
+
+ @Test
+ public void serializeIsPrivate() throws Exception {
+ assertRoundTrip(
+ newBuilder().columns(cols.toBuilder().isPrivate(true).build()).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto.toBuilder().setIsPrivate(true))
+ .build());
+ }
+
+ @Test
+ public void serializeIsWorkInProgress() throws Exception {
+ assertRoundTrip(
+ newBuilder().columns(cols.toBuilder().workInProgress(true).build()).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto.toBuilder().setWorkInProgress(true))
+ .build());
+ }
+
+ @Test
+ public void serializeHasReviewStarted() throws Exception {
+ assertRoundTrip(
+ newBuilder().columns(cols.toBuilder().reviewStarted(true).build()).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto.toBuilder().setReviewStarted(true))
+ .build());
+ }
+
+ @Test
+ public void serializeRevertOf() throws Exception {
+ assertRoundTrip(
+ newBuilder().columns(cols.toBuilder().revertOf(new Change.Id(999)).build()).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto.toBuilder().setRevertOf(999).setHasRevertOf(true))
+ .build());
+ }
+
+ @Test
+ public void serializePastAssignees() throws Exception {
+ assertRoundTrip(
+ newBuilder()
+ .pastAssignees(ImmutableSet.of(new Account.Id(2002), new Account.Id(2001)))
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addPastAssignee(2002)
+ .addPastAssignee(2001)
+ .build());
+ }
+
+ @Test
+ public void serializeHashtags() throws Exception {
+ assertRoundTrip(
+ newBuilder().hashtags(ImmutableSet.of("tag2", "tag1")).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addHashtag("tag2")
+ .addHashtag("tag1")
+ .build());
+ }
+
+ @Test
+ public void serializePatchSets() throws Exception {
+ PatchSet ps1 = new PatchSet(new PatchSet.Id(ID, 1));
+ ps1.setUploader(new Account.Id(2000));
+ ps1.setRevision(new RevId("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
+ ps1.setCreatedOn(cols.createdOn());
+ ByteString ps1Bytes = toByteString(ps1, PATCH_SET_CODEC);
+ assertThat(ps1Bytes.size()).isEqualTo(66);
+
+ PatchSet ps2 = new PatchSet(new PatchSet.Id(ID, 2));
+ ps2.setUploader(new Account.Id(3000));
+ ps2.setRevision(new RevId("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"));
+ ps2.setCreatedOn(cols.lastUpdatedOn());
+ ByteString ps2Bytes = toByteString(ps2, PATCH_SET_CODEC);
+ assertThat(ps2Bytes.size()).isEqualTo(66);
+ assertThat(ps2Bytes).isNotEqualTo(ps1Bytes);
+
+ assertRoundTrip(
+ newBuilder()
+ .patchSets(ImmutableMap.of(ps2.getId(), ps2, ps1.getId(), ps1).entrySet())
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addPatchSet(ps2Bytes)
+ .addPatchSet(ps1Bytes)
+ .build());
+ }
+
+ @Test
+ public void serializeApprovals() throws Exception {
+ PatchSetApproval a1 =
+ new PatchSetApproval(
+ new PatchSetApproval.Key(
+ new PatchSet.Id(ID, 1), new Account.Id(2001), new LabelId("Code-Review")),
+ (short) 1,
+ new Timestamp(1212L));
+ ByteString a1Bytes = toByteString(a1, APPROVAL_CODEC);
+ assertThat(a1Bytes.size()).isEqualTo(43);
+
+ PatchSetApproval a2 =
+ new PatchSetApproval(
+ new PatchSetApproval.Key(
+ new PatchSet.Id(ID, 1), new Account.Id(2002), new LabelId("Verified")),
+ (short) -1,
+ new Timestamp(3434L));
+ ByteString a2Bytes = toByteString(a2, APPROVAL_CODEC);
+ assertThat(a2Bytes.size()).isEqualTo(49);
+ assertThat(a2Bytes).isNotEqualTo(a1Bytes);
+
+ assertRoundTrip(
+ newBuilder()
+ .approvals(
+ ImmutableListMultimap.of(a2.getPatchSetId(), a2, a1.getPatchSetId(), a1).entries())
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addApproval(a2Bytes)
+ .addApproval(a1Bytes)
+ .build());
+ }
+
+ @Test
+ public void serializeReviewers() throws Exception {
+ assertRoundTrip(
+ newBuilder()
+ .reviewers(
+ ReviewerSet.fromTable(
+ ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
+ .put(ReviewerStateInternal.CC, new Account.Id(2001), new Timestamp(1212L))
+ .put(
+ ReviewerStateInternal.REVIEWER,
+ new Account.Id(2002),
+ new Timestamp(3434L))
+ .build()))
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addReviewer(
+ ReviewerSetEntryProto.newBuilder()
+ .setState("CC")
+ .setAccountId(2001)
+ .setTimestamp(1212L))
+ .addReviewer(
+ ReviewerSetEntryProto.newBuilder()
+ .setState("REVIEWER")
+ .setAccountId(2002)
+ .setTimestamp(3434L))
+ .build());
+ }
+
+ @Test
+ public void serializeReviewersByEmail() throws Exception {
+ assertRoundTrip(
+ newBuilder()
+ .reviewersByEmail(
+ ReviewerByEmailSet.fromTable(
+ ImmutableTable.<ReviewerStateInternal, Address, Timestamp>builder()
+ .put(
+ ReviewerStateInternal.CC,
+ new Address("Name1", "email1@example.com"),
+ new Timestamp(1212L))
+ .put(
+ ReviewerStateInternal.REVIEWER,
+ new Address("Name2", "email2@example.com"),
+ new Timestamp(3434L))
+ .build()))
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addReviewerByEmail(
+ ReviewerByEmailSetEntryProto.newBuilder()
+ .setState("CC")
+ .setAddress("Name1 <email1@example.com>")
+ .setTimestamp(1212L))
+ .addReviewerByEmail(
+ ReviewerByEmailSetEntryProto.newBuilder()
+ .setState("REVIEWER")
+ .setAddress("Name2 <email2@example.com>")
+ .setTimestamp(3434L))
+ .build());
+ }
+
+ @Test
+ public void serializeReviewersByEmailWithNullName() throws Exception {
+ ChangeNotesState actual =
+ assertRoundTrip(
+ newBuilder()
+ .reviewersByEmail(
+ ReviewerByEmailSet.fromTable(
+ ImmutableTable.of(
+ ReviewerStateInternal.CC,
+ new Address("emailonly@example.com"),
+ new Timestamp(1212L))))
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addReviewerByEmail(
+ ReviewerByEmailSetEntryProto.newBuilder()
+ .setState("CC")
+ .setAddress("emailonly@example.com")
+ .setTimestamp(1212L))
+ .build());
+
+ // Address doesn't consider the name field in equals, so we have to check it manually.
+ // TODO(dborowitz): Fix Address#equals.
+ ImmutableSet<Address> ccs = actual.reviewersByEmail().byState(ReviewerStateInternal.CC);
+ assertThat(ccs).hasSize(1);
+ Address address = Iterables.getOnlyElement(ccs);
+ assertThat(address.getName()).isNull();
+ assertThat(address.getEmail()).isEqualTo("emailonly@example.com");
+ }
+
+ @Test
+ public void serializePendingReviewers() throws Exception {
+ assertRoundTrip(
+ newBuilder()
+ .pendingReviewers(
+ ReviewerSet.fromTable(
+ ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
+ .put(ReviewerStateInternal.CC, new Account.Id(2001), new Timestamp(1212L))
+ .put(
+ ReviewerStateInternal.REVIEWER,
+ new Account.Id(2002),
+ new Timestamp(3434L))
+ .build()))
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addPendingReviewer(
+ ReviewerSetEntryProto.newBuilder()
+ .setState("CC")
+ .setAccountId(2001)
+ .setTimestamp(1212L))
+ .addPendingReviewer(
+ ReviewerSetEntryProto.newBuilder()
+ .setState("REVIEWER")
+ .setAccountId(2002)
+ .setTimestamp(3434L))
+ .build());
+ }
+
+ @Test
+ public void serializePendingReviewersByEmail() throws Exception {
+ assertRoundTrip(
+ newBuilder()
+ .pendingReviewersByEmail(
+ ReviewerByEmailSet.fromTable(
+ ImmutableTable.<ReviewerStateInternal, Address, Timestamp>builder()
+ .put(
+ ReviewerStateInternal.CC,
+ new Address("Name1", "email1@example.com"),
+ new Timestamp(1212L))
+ .put(
+ ReviewerStateInternal.REVIEWER,
+ new Address("Name2", "email2@example.com"),
+ new Timestamp(3434L))
+ .build()))
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addPendingReviewerByEmail(
+ ReviewerByEmailSetEntryProto.newBuilder()
+ .setState("CC")
+ .setAddress("Name1 <email1@example.com>")
+ .setTimestamp(1212L))
+ .addPendingReviewerByEmail(
+ ReviewerByEmailSetEntryProto.newBuilder()
+ .setState("REVIEWER")
+ .setAddress("Name2 <email2@example.com>")
+ .setTimestamp(3434L))
+ .build());
+ }
+
+ @Test
+ public void serializeAllPastReviewers() throws Exception {
+ assertRoundTrip(
+ newBuilder()
+ .allPastReviewers(ImmutableList.of(new Account.Id(2002), new Account.Id(2001)))
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addPastReviewer(2002)
+ .addPastReviewer(2001)
+ .build());
+ }
+
+ @Test
+ public void serializeReviewerUpdates() throws Exception {
+ assertRoundTrip(
+ newBuilder()
+ .reviewerUpdates(
+ ImmutableList.of(
+ ReviewerStatusUpdate.create(
+ new Timestamp(1212L),
+ new Account.Id(1000),
+ new Account.Id(2002),
+ ReviewerStateInternal.CC),
+ ReviewerStatusUpdate.create(
+ new Timestamp(3434L),
+ new Account.Id(1000),
+ new Account.Id(2001),
+ ReviewerStateInternal.REVIEWER)))
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addReviewerUpdate(
+ ReviewerStatusUpdateProto.newBuilder()
+ .setDate(1212L)
+ .setUpdatedBy(1000)
+ .setReviewer(2002)
+ .setState("CC"))
+ .addReviewerUpdate(
+ ReviewerStatusUpdateProto.newBuilder()
+ .setDate(3434L)
+ .setUpdatedBy(1000)
+ .setReviewer(2001)
+ .setState("REVIEWER"))
+ .build());
+ }
+
+ @Test
+ public void serializeSubmitRecords() throws Exception {
+ SubmitRecord sr1 = new SubmitRecord();
+ sr1.status = SubmitRecord.Status.OK;
+
+ SubmitRecord sr2 = new SubmitRecord();
+ sr2.status = SubmitRecord.Status.FORCED;
+
+ assertRoundTrip(
+ newBuilder().submitRecords(ImmutableList.of(sr2, sr1)).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addSubmitRecord("{\"status\":\"FORCED\"}")
+ .addSubmitRecord("{\"status\":\"OK\"}")
+ .build());
+ }
+
+ @Test
+ public void serializeChangeMessages() throws Exception {
+ ChangeMessage m1 =
+ new ChangeMessage(
+ new ChangeMessage.Key(ID, "uuid1"),
+ new Account.Id(1000),
+ new Timestamp(1212L),
+ new PatchSet.Id(ID, 1));
+ ByteString m1Bytes = toByteString(m1, MESSAGE_CODEC);
+ assertThat(m1Bytes.size()).isEqualTo(35);
+
+ ChangeMessage m2 =
+ new ChangeMessage(
+ new ChangeMessage.Key(ID, "uuid2"),
+ new Account.Id(2000),
+ new Timestamp(3434L),
+ new PatchSet.Id(ID, 2));
+ ByteString m2Bytes = toByteString(m2, MESSAGE_CODEC);
+ assertThat(m2Bytes.size()).isEqualTo(35);
+ assertThat(m2Bytes).isNotEqualTo(m1Bytes);
+
+ assertRoundTrip(
+ newBuilder().changeMessages(ImmutableList.of(m2, m1)).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addChangeMessage(m2Bytes)
+ .addChangeMessage(m1Bytes)
+ .build());
+ }
+
+ @Test
+ public void serializePublishedComments() throws Exception {
+ Comment c1 =
+ new Comment(
+ new Comment.Key("uuid1", "file1", 1),
+ new Account.Id(1001),
+ new Timestamp(1212L),
+ (short) 1,
+ "message 1",
+ "serverId",
+ false);
+ c1.setRevId(new RevId("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
+ String c1Json = Serializer.GSON.toJson(c1);
+
+ Comment c2 =
+ new Comment(
+ new Comment.Key("uuid2", "file2", 2),
+ new Account.Id(1002),
+ new Timestamp(3434L),
+ (short) 2,
+ "message 2",
+ "serverId",
+ true);
+ c2.setRevId(new RevId("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"));
+ String c2Json = Serializer.GSON.toJson(c2);
+
+ assertRoundTrip(
+ newBuilder()
+ .publishedComments(
+ ImmutableListMultimap.of(new RevId(c2.revId), c2, new RevId(c1.revId), c1))
+ .build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .addPublishedComment(c2Json)
+ .addPublishedComment(c1Json)
+ .build());
+ }
+
+ @Test
+ public void serializeReadOnlyUntil() throws Exception {
+ assertRoundTrip(
+ newBuilder().readOnlyUntil(new Timestamp(1212L)).build(),
+ ChangeNotesStateProto.newBuilder()
+ .setMetaId(SHA_BYTES)
+ .setChangeId(ID.get())
+ .setColumns(colsProto)
+ .setReadOnlyUntil(1212L)
+ .setHasReadOnlyUntil(true)
+ .build());
+ }
+
+ @Test
+ public void changeNotesStateMethods() throws Exception {
+ assertThatSerializedClass(ChangeNotesState.class)
+ .hasAutoValueMethods(
+ ImmutableMap.<String, Type>builder()
+ .put("metaId", ObjectId.class)
+ .put("changeId", Change.Id.class)
+ .put("columns", ChangeColumns.class)
+ .put("pastAssignees", new TypeLiteral<ImmutableSet<Account.Id>>() {}.getType())
+ .put("hashtags", new TypeLiteral<ImmutableSet<String>>() {}.getType())
+ .put(
+ "patchSets",
+ new TypeLiteral<ImmutableList<Map.Entry<PatchSet.Id, PatchSet>>>() {}.getType())
+ .put(
+ "approvals",
+ new TypeLiteral<
+ ImmutableList<Map.Entry<PatchSet.Id, PatchSetApproval>>>() {}.getType())
+ .put("reviewers", ReviewerSet.class)
+ .put("reviewersByEmail", ReviewerByEmailSet.class)
+ .put("pendingReviewers", ReviewerSet.class)
+ .put("pendingReviewersByEmail", ReviewerByEmailSet.class)
+ .put("allPastReviewers", new TypeLiteral<ImmutableList<Account.Id>>() {}.getType())
+ .put(
+ "reviewerUpdates",
+ new TypeLiteral<ImmutableList<ReviewerStatusUpdate>>() {}.getType())
+ .put("submitRecords", new TypeLiteral<ImmutableList<SubmitRecord>>() {}.getType())
+ .put("changeMessages", new TypeLiteral<ImmutableList<ChangeMessage>>() {}.getType())
+ .put(
+ "publishedComments",
+ new TypeLiteral<ImmutableListMultimap<RevId, Comment>>() {}.getType())
+ .put("readOnlyUntil", Timestamp.class)
+ .build());
+ }
+
+ @Test
+ public void changeColumnsMethods() throws Exception {
+ assertThatSerializedClass(ChangeColumns.class)
+ .hasAutoValueMethods(
+ ImmutableMap.<String, Type>builder()
+ .put("changeKey", Change.Key.class)
+ .put("createdOn", Timestamp.class)
+ .put("lastUpdatedOn", Timestamp.class)
+ .put("owner", Account.Id.class)
+ .put("branch", String.class)
+ .put("currentPatchSetId", PatchSet.Id.class)
+ .put("subject", String.class)
+ .put("topic", String.class)
+ .put("originalSubject", String.class)
+ .put("submissionId", String.class)
+ .put("assignee", Account.Id.class)
+ .put("status", Change.Status.class)
+ .put("isPrivate", boolean.class)
+ .put("workInProgress", boolean.class)
+ .put("reviewStarted", boolean.class)
+ .put("revertOf", Change.Id.class)
+ .put("toBuilder", ChangeNotesState.ChangeColumns.Builder.class)
+ .build());
+ }
+
+ @Test
+ public void patchSetFields() throws Exception {
+ assertThatSerializedClass(PatchSet.class)
+ .hasFields(
+ ImmutableMap.<String, Type>builder()
+ .put("id", PatchSet.Id.class)
+ .put("revision", RevId.class)
+ .put("uploader", Account.Id.class)
+ .put("createdOn", Timestamp.class)
+ .put("groups", String.class)
+ .put("pushCertificate", String.class)
+ .put("description", String.class)
+ .build());
+ }
+
+ @Test
+ public void patchSetApprovalFields() throws Exception {
+ assertThatSerializedClass(PatchSetApproval.Key.class)
+ .hasFields(
+ ImmutableMap.<String, Type>builder()
+ .put("patchSetId", PatchSet.Id.class)
+ .put("accountId", Account.Id.class)
+ .put("categoryId", LabelId.class)
+ .build());
+ assertThatSerializedClass(PatchSetApproval.class)
+ .hasFields(
+ ImmutableMap.<String, Type>builder()
+ .put("key", PatchSetApproval.Key.class)
+ .put("value", short.class)
+ .put("granted", Timestamp.class)
+ .put("tag", String.class)
+ .put("realAccountId", Account.Id.class)
+ .put("postSubmit", boolean.class)
+ .build());
+ }
+
+ @Test
+ public void reviewerSetFields() throws Exception {
+ assertThatSerializedClass(ReviewerSet.class)
+ .hasFields(
+ ImmutableMap.of(
+ "table",
+ new TypeLiteral<
+ ImmutableTable<
+ ReviewerStateInternal, Account.Id, Timestamp>>() {}.getType(),
+ "accounts", new TypeLiteral<ImmutableSet<Account.Id>>() {}.getType()));
+ }
+
+ @Test
+ public void reviewerByEmailSetFields() throws Exception {
+ assertThatSerializedClass(ReviewerByEmailSet.class)
+ .hasFields(
+ ImmutableMap.of(
+ "table",
+ new TypeLiteral<
+ ImmutableTable<ReviewerStateInternal, Address, Timestamp>>() {}.getType(),
+ "users", new TypeLiteral<ImmutableSet<Address>>() {}.getType()));
+ }
+
+ @Test
+ public void reviewerStatusUpdateMethods() throws Exception {
+ assertThatSerializedClass(ReviewerStatusUpdate.class)
+ .hasAutoValueMethods(
+ ImmutableMap.of(
+ "date", Timestamp.class,
+ "updatedBy", Account.Id.class,
+ "reviewer", Account.Id.class,
+ "state", ReviewerStateInternal.class));
+ }
+
+ @Test
+ public void submitRecordFields() throws Exception {
+ assertThatSerializedClass(SubmitRecord.class)
+ .hasFields(
+ ImmutableMap.of(
+ "status",
+ SubmitRecord.Status.class,
+ "labels",
+ new TypeLiteral<List<SubmitRecord.Label>>() {}.getType(),
+ "requirements",
+ new TypeLiteral<List<SubmitRequirement>>() {}.getType(),
+ "errorMessage",
+ String.class));
+ assertThatSerializedClass(SubmitRecord.Label.class)
+ .hasFields(
+ ImmutableMap.of(
+ "label", String.class,
+ "status", SubmitRecord.Label.Status.class,
+ "appliedBy", Account.Id.class));
+ assertThatSerializedClass(SubmitRequirement.class)
+ .hasAutoValueMethods(
+ ImmutableMap.of(
+ "fallbackText", String.class,
+ "type", String.class,
+ "data", new TypeLiteral<ImmutableMap<String, String>>() {}.getType()));
+ }
+
+ @Test
+ public void changeMessageFields() throws Exception {
+ assertThatSerializedClass(ChangeMessage.Key.class)
+ .hasFields(ImmutableMap.of("changeId", Change.Id.class, "uuid", String.class));
+ assertThatSerializedClass(ChangeMessage.class)
+ .hasFields(
+ ImmutableMap.<String, Type>builder()
+ .put("key", ChangeMessage.Key.class)
+ .put("author", Account.Id.class)
+ .put("writtenOn", Timestamp.class)
+ .put("message", String.class)
+ .put("patchset", PatchSet.Id.class)
+ .put("tag", String.class)
+ .put("realAuthor", Account.Id.class)
+ .build());
+ }
+
+ @Test
+ public void commentFields() throws Exception {
+ assertThatSerializedClass(Comment.Key.class)
+ .hasFields(
+ ImmutableMap.of(
+ "uuid", String.class, "filename", String.class, "patchSetId", int.class));
+ assertThatSerializedClass(Comment.Identity.class).hasFields(ImmutableMap.of("id", int.class));
+ assertThatSerializedClass(Comment.Range.class)
+ .hasFields(
+ ImmutableMap.of(
+ "startLine", int.class,
+ "startChar", int.class,
+ "endLine", int.class,
+ "endChar", int.class));
+ assertThatSerializedClass(Comment.class)
+ .hasFields(
+ ImmutableMap.<String, Type>builder()
+ .put("key", Comment.Key.class)
+ .put("lineNbr", int.class)
+ .put("author", Comment.Identity.class)
+ .put("realAuthor", Comment.Identity.class)
+ .put("writtenOn", Timestamp.class)
+ .put("side", short.class)
+ .put("message", String.class)
+ .put("parentUuid", String.class)
+ .put("range", Comment.Range.class)
+ .put("tag", String.class)
+ .put("revId", String.class)
+ .put("serverId", String.class)
+ .put("unresolved", boolean.class)
+ .put("legacyFormat", boolean.class)
+ .build());
+ }
+
+ private static ChangeNotesStateProto toProto(ChangeNotesState state) throws Exception {
+ return ChangeNotesStateProto.parseFrom(Serializer.INSTANCE.serialize(state));
+ }
+
+ private static ChangeNotesState assertRoundTrip(
+ ChangeNotesState state, ChangeNotesStateProto expectedProto) throws Exception {
+ ChangeNotesStateProto actualProto = toProto(state);
+ assertThat(actualProto).isEqualTo(expectedProto);
+ ChangeNotesState actual = Serializer.INSTANCE.deserialize(Serializer.INSTANCE.serialize(state));
+ assertThat(actual).isEqualTo(state);
+ // It's possible that ChangeNotesState contains objects which implement equals without taking
+ // into account all fields. Return the actual deserialized instance so that callers can perform
+ // additional assertions if necessary.
+ return actual;
+ }
+
+ private static ByteString toByteString(ObjectId id) {
+ byte[] buf = new byte[Constants.OBJECT_ID_LENGTH];
+ id.copyRawTo(buf, 0);
+ return ByteString.copyFrom(buf);
+ }
+
+ private <T> ByteString toByteString(T object, ProtobufCodec<T> codec) {
+ return ProtoCacheSerializers.toByteString(object, codec);
+ }
+}
diff --git a/lib/truth/BUILD b/lib/truth/BUILD
index cb17269..82cd98a 100644
--- a/lib/truth/BUILD
+++ b/lib/truth/BUILD
@@ -19,3 +19,31 @@
"//lib:guava",
],
)
+
+java_library(
+ name = "truth-liteproto-extension",
+ data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
+ visibility = ["//visibility:private"],
+ exports = ["@truth-liteproto-extension//jar"],
+ runtime_deps = [
+ ":truth",
+ "//lib:guava",
+ "//lib:protobuf",
+ ],
+)
+
+java_library(
+ name = "truth-proto-extension",
+ data = ["//lib:LICENSE-DO_NOT_DISTRIBUTE"],
+ visibility = ["//visibility:public"],
+ exports = [
+ ":truth-liteproto-extension",
+ "@truth-proto-extension//jar",
+ ],
+ runtime_deps = [
+ ":truth",
+ ":truth-liteproto-extension",
+ "//lib:guava",
+ "//lib:protobuf",
+ ],
+)
diff --git a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html
index cf79a31..b824f1c 100644
--- a/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html
+++ b/polygerrit-ui/app/elements/change/gr-included-in-dialog/gr-included-in-dialog.html
@@ -30,6 +30,7 @@
padding: 4.5em 1em 1em 1em;
}
header {
+ background-color: var(--dialog-background-color);
border-bottom: 1px solid var(--border-color);
left: 0;
padding: 1em;
diff --git a/proto/cache.proto b/proto/cache.proto
index 634b595..7e2e75a 100644
--- a/proto/cache.proto
+++ b/proto/cache.proto
@@ -45,3 +45,142 @@
int64 expires_at = 4;
string provider_id = 5;
}
+
+
+// Serialized form of com.google.gerrit.server.notedb.ChangeNotesCache.Key.
+// Next ID: 4
+message ChangeNotesKeyProto {
+ string project = 1;
+ int32 change_id = 2;
+ bytes id = 3;
+}
+
+// Serialized from of com.google.gerrit.server.notedb.ChangeNotesState.
+//
+// Note on embedded protos: this is just for storing in a cache, so some formats
+// were chosen ease of coding the initial implementation. In particular, where
+// there already exists another serialization mechanism in Gerrit for
+// serializing a particular field, we use that rather than defining a new proto
+// type. This includes ReviewDb types that can be serialized to proto using
+// ProtobufCodec as well as NoteDb and indexed types that are serialized using
+// JSON. We can always revisit this decision later, particularly when we
+// eliminate the ReviewDb types; it just requires bumping the cache version.
+//
+// Note on nullability: there are a lot of nullable fields in ChangeNotesState
+// and its dependencies. It's likely we could make some of them non-nullable,
+// but each one of those would be a potentially significant amount of cleanup,
+// and there's no guarantee we'd be able to eliminate all of them. (For a less
+// complex class, it's likely the cleanup would be more feasible.)
+//
+// Instead, we just take the tedious yet simple approach of having a "has_foo"
+// field for each nullable field "foo", indicating whether or not foo is null.
+//
+// Next ID: 19
+message ChangeNotesStateProto {
+ // Effectively required, even though the corresponding ChangeNotesState field
+ // is optional, since the field is only absent when NoteDb is disabled, in
+ // which case attempting to use the ChangeNotesCache is programmer error.
+ bytes meta_id = 1;
+
+ int32 change_id = 2;
+
+ // Next ID: 24
+ message ChangeColumnsProto {
+ string change_key = 1;
+
+ int64 created_on = 2;
+
+ int64 last_updated_on = 3;
+
+ int32 owner = 4;
+
+ string branch = 5;
+
+ int32 current_patch_set_id = 6;
+ bool has_current_patch_set_id = 7;
+
+ string subject = 8;
+
+ string topic = 9;
+ bool has_topic = 10;
+
+ string original_subject = 11;
+ bool has_original_subject = 12;
+
+ string submission_id = 13;
+ bool has_submission_id = 14;
+
+ int32 assignee = 15;
+ bool has_assignee = 16;
+
+ string status = 17;
+ bool has_status = 18;
+
+ bool is_private = 19;
+
+ bool work_in_progress = 20;
+
+ bool review_started = 21;
+
+ int32 revert_of = 22;
+ bool has_revert_of = 23;
+ }
+ // Effectively required, even though the corresponding ChangeNotesState field
+ // is optional, since the field is only absent when NoteDb is disabled, in
+ // which case attempting to use the ChangeNotesCache is programmer error.
+ ChangeColumnsProto columns = 3;
+
+ repeated int32 past_assignee = 4;
+
+ repeated string hashtag = 5;
+
+ // Raw PatchSet proto as produced by ProtobufCodec.
+ repeated bytes patch_set = 6;
+
+ // Raw PatchSetApproval proto as produced by ProtobufCodec.
+ repeated bytes approval = 7;
+
+ // Next ID: 4
+ message ReviewerSetEntryProto {
+ string state = 1;
+ int32 account_id = 2;
+ int64 timestamp = 3;
+ }
+ repeated ReviewerSetEntryProto reviewer = 8;
+
+ // Next ID: 4
+ message ReviewerByEmailSetEntryProto {
+ string state = 1;
+ string address = 2;
+ int64 timestamp = 3;
+ }
+ repeated ReviewerByEmailSetEntryProto reviewer_by_email = 9;
+
+ repeated ReviewerSetEntryProto pending_reviewer = 10;
+
+ repeated ReviewerByEmailSetEntryProto pending_reviewer_by_email = 11;
+
+ repeated int32 past_reviewer = 12;
+
+ // Next ID: 5
+ message ReviewerStatusUpdateProto {
+ int64 date = 1;
+ int32 updated_by = 2;
+ int32 reviewer = 3;
+ string state = 4;
+ }
+ repeated ReviewerStatusUpdateProto reviewer_update = 13;
+
+ // JSON produced from
+ // com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord.
+ repeated string submit_record = 14;
+
+ // Raw ChangeMessage proto as produced by ProtobufCodec.
+ repeated bytes change_message = 15;
+
+ // JSON produced from com.google.gerrit.reviewdb.client.Comment.
+ repeated string published_comment = 16;
+
+ int64 read_only_until = 17;
+ bool has_read_only_until = 18;
+}