blob: eb6c15a2aa1080b82c873859dea31a48366a28e6 [file] [log] [blame]
// 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.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
import static java.util.Objects.requireNonNull;
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.ImmutableSortedMap;
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.entities.Account;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.AttentionSetUpdate;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.ChangeMessage;
import com.google.gerrit.entities.HumanComment;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.entities.converter.ChangeMessageProtoConverter;
import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
import com.google.gerrit.entities.converter.PatchSetProtoConverter;
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.proto.Protos;
import com.google.gerrit.server.ReviewerByEmailSet;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.ReviewerStatusUpdate;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto;
import com.google.gerrit.server.cache.proto.Cache.ChangeNotesStateProto.AttentionSetUpdateProto;
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.cache.serialize.CacheSerializer;
import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
import com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord;
import com.google.gson.Gson;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.lib.ObjectId;
/**
* Immutable state associated with a change meta ref at a given commit.
*
* <p>One instance is the output of a single {@link ChangeNotesParser}, and contains types required
* to support public methods on {@link ChangeNotes}. It is intended to be cached in-process.
*
* <p>When new fields are added to the {@link ChangeNotesState}, {@link
* ChangeNotesCache.Weigher#weigh} should be updated.
*
* <p>Note that {@link ChangeNotes} contains more than just a single {@code ChangeNoteState}, such
* as per-draft information, so that class is not cached directly.
*/
// TODO(paiking): This class should be refactored to get rid of potentially duplicate or unneeded
// variables, such as allAttentionSetUpdates, reviewerUpdates, and others.
@AutoValue
public abstract class ChangeNotesState {
static ChangeNotesState empty(Change change) {
return Builder.empty(change.getId()).build();
}
private static Builder builder() {
return new AutoValue_ChangeNotesState.Builder();
}
static ChangeNotesState create(
ObjectId metaId,
Change.Id changeId,
Change.Key changeKey,
Instant createdOn,
Instant lastUpdatedOn,
Account.Id owner,
String serverId,
String branch,
@Nullable PatchSet.Id currentPatchSetId,
String subject,
@Nullable String topic,
@Nullable String originalSubject,
@Nullable String submissionId,
@Nullable Change.Status status,
Set<String> hashtags,
ImmutableSortedMap<String, String> customKeyedValues,
Map<PatchSet.Id, PatchSet> patchSets,
ListMultimap<PatchSet.Id, PatchSetApproval> approvals,
ReviewerSet reviewers,
ReviewerByEmailSet reviewersByEmail,
ReviewerSet pendingReviewers,
ReviewerByEmailSet pendingReviewersByEmail,
List<Account.Id> allPastReviewers,
List<ReviewerStatusUpdate> reviewerUpdates,
Set<AttentionSetUpdate> attentionSetUpdates,
List<AttentionSetUpdate> allAttentionSetUpdates,
List<SubmitRecord> submitRecords,
List<ChangeMessage> changeMessages,
ListMultimap<ObjectId, HumanComment> publishedComments,
List<SubmitRequirementResult> submitRequirementResults,
boolean isPrivate,
boolean workInProgress,
boolean reviewStarted,
@Nullable Change.Id revertOf,
@Nullable PatchSet.Id cherryPickOf,
int updateCount,
@Nullable Instant mergedOn) {
requireNonNull(
metaId,
() ->
String.format(
"metaId is required when passing arguments to create(...)."
+ " To create an empty %s without"
+ " NoteDb data, use empty(...) instead",
ChangeNotesState.class.getSimpleName()));
return builder()
.metaId(metaId)
.changeId(changeId)
.columns(
ChangeColumns.builder()
.changeKey(changeKey)
.createdOn(createdOn)
.lastUpdatedOn(lastUpdatedOn)
.owner(owner)
.branch(branch)
.status(status)
.currentPatchSetId(currentPatchSetId)
.subject(subject)
.topic(topic)
.originalSubject(originalSubject)
.submissionId(submissionId)
.isPrivate(isPrivate)
.workInProgress(workInProgress)
.reviewStarted(reviewStarted)
.revertOf(revertOf)
.cherryPickOf(cherryPickOf)
.build())
.hashtags(hashtags)
.customKeyedValues(customKeyedValues.entrySet())
.serverId(serverId)
.patchSets(patchSets.entrySet())
.approvals(approvals.entries())
.reviewers(reviewers)
.reviewersByEmail(reviewersByEmail)
.pendingReviewers(pendingReviewers)
.pendingReviewersByEmail(pendingReviewersByEmail)
.allPastReviewers(allPastReviewers)
.reviewerUpdates(reviewerUpdates)
.attentionSet(attentionSetUpdates)
.allAttentionSetUpdates(allAttentionSetUpdates)
.submitRecords(submitRecords)
.changeMessages(changeMessages)
.publishedComments(publishedComments)
.submitRequirementsResult(submitRequirementResults)
.updateCount(updateCount)
.mergedOn(mergedOn)
.build();
}
/**
* Subset of Change columns that can be represented in NoteDb.
*
* <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 Instant createdOn();
abstract Instant lastUpdatedOn();
abstract Account.Id owner();
// 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();
abstract String subject();
@Nullable
abstract String topic();
@Nullable
abstract String originalSubject();
@Nullable
abstract String submissionId();
abstract boolean isPrivate();
abstract boolean workInProgress();
abstract boolean reviewStarted();
@Nullable
abstract Change.Id revertOf();
@Nullable
abstract PatchSet.Id cherryPickOf();
abstract Builder toBuilder();
@AutoValue.Builder
abstract static class Builder {
abstract Builder changeKey(Change.Key changeKey);
abstract Builder createdOn(Instant createdOn);
abstract Builder lastUpdatedOn(Instant lastUpdatedOn);
abstract Builder owner(Account.Id owner);
abstract Builder branch(String branch);
abstract Builder currentPatchSetId(@Nullable PatchSet.Id currentPatchSetId);
abstract Builder subject(String subject);
abstract Builder topic(@Nullable String topic);
abstract Builder originalSubject(@Nullable String originalSubject);
abstract Builder submissionId(@Nullable String submissionId);
abstract Builder status(@Nullable Change.Status status);
abstract Builder isPrivate(boolean isPrivate);
abstract Builder workInProgress(boolean workInProgress);
abstract Builder reviewStarted(boolean reviewStarted);
abstract Builder revertOf(@Nullable Change.Id revertOf);
abstract Builder cherryPickOf(@Nullable PatchSet.Id cherryPickOf);
abstract ChangeColumns build();
}
}
// Only null if NoteDb is disabled.
@Nullable
abstract ObjectId metaId();
abstract Change.Id changeId();
// Only null if NoteDb is disabled.
@Nullable
abstract ChangeColumns columns();
// Other related to this Change.
abstract ImmutableSet<String> hashtags();
/*
Custom values are small key value pairs. They can be used to associate the
change with external, potentially proprietary systems (e.g. Bug trackers)
without requiring dedicated fields in Gerrit-core.
This data is visible to everyone who can see the change. It must not contain
personally identify-able information.
*/
abstract ImmutableList<Map.Entry<String, String>> customKeyedValues();
@Nullable
abstract String serverId();
abstract ImmutableList<Map.Entry<PatchSet.Id, PatchSet>> patchSets();
abstract ImmutableList<Map.Entry<PatchSet.Id, PatchSetApproval>> approvals();
abstract ReviewerSet reviewers();
abstract ReviewerByEmailSet reviewersByEmail();
abstract ReviewerSet pendingReviewers();
abstract ReviewerByEmailSet pendingReviewersByEmail();
abstract ImmutableList<Account.Id> allPastReviewers();
abstract ImmutableList<ReviewerStatusUpdate> reviewerUpdates();
/** Returns the most recent update (i.e. current status) per user. */
abstract ImmutableSet<AttentionSetUpdate> attentionSet();
/** Returns all attention set updates. */
abstract ImmutableList<AttentionSetUpdate> allAttentionSetUpdates();
abstract ImmutableList<SubmitRecord> submitRecords();
abstract ImmutableList<ChangeMessage> changeMessages();
abstract ImmutableListMultimap<ObjectId, HumanComment> publishedComments();
abstract ImmutableList<SubmitRequirementResult> submitRequirementsResult();
abstract int updateCount();
@Nullable
abstract Instant mergedOn();
Change newChange(Project.NameKey project) {
ChangeColumns c = requireNonNull(columns(), "columns are required");
Change change =
new Change(
c.changeKey(),
changeId(),
c.owner(),
BranchNameKey.create(project, c.branch()),
c.createdOn());
copyNonConstructorColumnsTo(change);
return change;
}
void copyColumnsTo(Change change) {
ChangeColumns c = columns();
checkState(
c != null && metaId() != null,
"missing columns or metaId in ChangeNotesState; is NoteDb enabled? %s",
this);
change.setKey(c.changeKey());
change.setOwner(c.owner());
change.setDest(BranchNameKey.create(change.getProject(), c.branch()));
change.setCreatedOn(c.createdOn());
change.setServerId(serverId());
copyNonConstructorColumnsTo(change);
}
private void copyNonConstructorColumnsTo(Change change) {
ChangeColumns c = requireNonNull(columns(), "columns are required");
if (c.status() != null) {
change.setStatus(c.status());
}
change.setTopic(Strings.emptyToNull(c.topic()));
change.setLastUpdatedOn(c.lastUpdatedOn());
change.setSubmissionId(c.submissionId());
change.setPrivate(c.isPrivate());
change.setWorkInProgress(c.workInProgress());
change.setReviewStarted(c.reviewStarted());
change.setRevertOf(c.revertOf());
change.setCherryPickOf(c.cherryPickOf());
if (!patchSets().isEmpty()) {
change.setCurrentPatchSet(c.currentPatchSetId(), c.subject(), c.originalSubject());
} else {
// TODO(dborowitz): This should be an error, but for now it's required for
// some tests to pass.
change.clearCurrentPatchSet();
}
}
@AutoValue.Builder
abstract static class Builder {
static Builder empty(Change.Id changeId) {
return new AutoValue_ChangeNotesState.Builder()
.changeId(changeId)
.hashtags(ImmutableSet.of())
.customKeyedValues(ImmutableList.of())
.patchSets(ImmutableList.of())
.approvals(ImmutableList.of())
.reviewers(ReviewerSet.empty())
.reviewersByEmail(ReviewerByEmailSet.empty())
.pendingReviewers(ReviewerSet.empty())
.pendingReviewersByEmail(ReviewerByEmailSet.empty())
.allPastReviewers(ImmutableList.of())
.reviewerUpdates(ImmutableList.of())
.attentionSet(ImmutableSet.of())
.allAttentionSetUpdates(ImmutableList.of())
.submitRecords(ImmutableList.of())
.changeMessages(ImmutableList.of())
.publishedComments(ImmutableListMultimap.of())
.submitRequirementsResult(ImmutableList.of())
.updateCount(0);
}
abstract Builder metaId(ObjectId metaId);
abstract Builder changeId(Change.Id changeId);
abstract Builder columns(ChangeColumns columns);
abstract Builder serverId(String serverId);
abstract Builder hashtags(Iterable<String> hashtags);
abstract Builder customKeyedValues(Iterable<Map.Entry<String, String>> customKeyedValues);
abstract Builder patchSets(Iterable<Map.Entry<PatchSet.Id, PatchSet>> patchSets);
abstract Builder approvals(Iterable<Map.Entry<PatchSet.Id, PatchSetApproval>> approvals);
abstract Builder reviewers(ReviewerSet reviewers);
abstract Builder reviewersByEmail(ReviewerByEmailSet reviewersByEmail);
abstract Builder pendingReviewers(ReviewerSet pendingReviewers);
abstract Builder pendingReviewersByEmail(ReviewerByEmailSet pendingReviewersByEmail);
abstract Builder allPastReviewers(List<Account.Id> allPastReviewers);
abstract Builder reviewerUpdates(List<ReviewerStatusUpdate> reviewerUpdates);
abstract Builder attentionSet(Set<AttentionSetUpdate> attentionSetUpdates);
abstract Builder allAttentionSetUpdates(List<AttentionSetUpdate> attentionSetUpdates);
abstract Builder submitRecords(List<SubmitRecord> submitRecords);
abstract Builder changeMessages(List<ChangeMessage> changeMessages);
abstract Builder publishedComments(ListMultimap<ObjectId, HumanComment> publishedComments);
abstract Builder submitRequirementsResult(
List<SubmitRequirementResult> submitRequirementsResult);
abstract Builder updateCount(int updateCount);
abstract Builder mergedOn(Instant mergedOn);
abstract ChangeNotesState build();
}
/**
* Convert ChangeNotesState (which is AutoValue based) to byte[] and back, using protocol buffers.
*/
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();
b.setMetaId(ObjectIdConverter.create().toByteString(object.metaId()))
.setChangeId(object.changeId().get())
.setColumns(toChangeColumnsProto(object.columns()));
if (object.serverId() != null) {
b.setServerId(object.serverId());
b.setHasServerId(true);
}
object.hashtags().forEach(b::addHashtag);
object
.customKeyedValues()
.forEach(
entry -> {
b.putCustomKeyedValues(entry.getKey(), entry.getValue());
});
object
.patchSets()
.forEach(e -> b.addPatchSet(PatchSetProtoConverter.INSTANCE.toProto(e.getValue())));
object
.approvals()
.forEach(
e -> b.addApproval(PatchSetApprovalProtoConverter.INSTANCE.toProto(e.getValue())));
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.attentionSet().forEach(u -> b.addAttentionSetUpdate(toAttentionSetUpdateProto(u)));
object
.allAttentionSetUpdates()
.forEach(u -> b.addAllAttentionSetUpdate(toAttentionSetUpdateProto(u)));
object
.submitRecords()
.forEach(r -> b.addSubmitRecord(GSON.toJson(new StoredSubmitRecord(r))));
object
.changeMessages()
.forEach(m -> b.addChangeMessage(ChangeMessageProtoConverter.INSTANCE.toProto(m)));
object.publishedComments().values().forEach(c -> b.addPublishedComment(GSON.toJson(c)));
object
.submitRequirementsResult()
.forEach(
sr ->
b.addSubmitRequirementResult(
SubmitRequirementProtoConverter.INSTANCE.toProto(sr)));
b.setUpdateCount(object.updateCount());
if (object.mergedOn() != null) {
b.setMergedOnMillis(object.mergedOn().toEpochMilli());
b.setHasMergedOn(true);
}
return Protos.toByteArray(b.build());
}
private static ChangeColumnsProto toChangeColumnsProto(ChangeColumns cols) {
ChangeColumnsProto.Builder b =
ChangeColumnsProto.newBuilder()
.setChangeKey(cols.changeKey().get())
.setCreatedOnMillis(cols.createdOn().toEpochMilli())
.setLastUpdatedOnMillis(cols.lastUpdatedOn().toEpochMilli())
.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.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);
}
if (cols.cherryPickOf() != null) {
b.setCherryPickOf(cols.cherryPickOf().getCommaSeparatedChangeAndPatchSetId())
.setHasCherryPickOf(true);
}
return b.build();
}
private static ReviewerSetEntryProto toReviewerSetEntry(
Table.Cell<ReviewerStateInternal, Account.Id, Instant> c) {
return ReviewerSetEntryProto.newBuilder()
.setState(REVIEWER_STATE_CONVERTER.reverse().convert(c.getRowKey()))
.setAccountId(c.getColumnKey().get())
.setTimestampMillis(c.getValue().toEpochMilli())
.build();
}
private static ReviewerByEmailSetEntryProto toReviewerByEmailSetEntry(
Table.Cell<ReviewerStateInternal, Address, Instant> c) {
return ReviewerByEmailSetEntryProto.newBuilder()
.setState(REVIEWER_STATE_CONVERTER.reverse().convert(c.getRowKey()))
.setAddress(c.getColumnKey().toHeaderString())
.setTimestampMillis(c.getValue().toEpochMilli())
.build();
}
private static ReviewerStatusUpdateProto toReviewerStatusUpdateProto(ReviewerStatusUpdate u) {
ReviewerStatusUpdateProto.Builder protoBuilder =
ReviewerStatusUpdateProto.newBuilder()
.setTimestampMillis(u.date().toEpochMilli())
.setUpdatedBy(u.updatedBy().get())
.setState(REVIEWER_STATE_CONVERTER.reverse().convert(u.state()));
u.reviewer()
.ifPresent(
accountId -> {
protoBuilder.setHasReviewer(true);
protoBuilder.setReviewer(accountId.get());
});
u.reviewerByEmail()
.ifPresent(
address -> {
protoBuilder.setHasReviewerByEmail(true);
protoBuilder.setReviewerByEmail(address.toHeaderString());
});
return protoBuilder.build();
}
private static AttentionSetUpdateProto toAttentionSetUpdateProto(
AttentionSetUpdate attentionSetUpdate) {
return AttentionSetUpdateProto.newBuilder()
.setTimestampMillis(attentionSetUpdate.timestamp().toEpochMilli())
.setAccount(attentionSetUpdate.account().get())
.setOperation(attentionSetUpdate.operation().name())
.setReason(attentionSetUpdate.reason())
.build();
}
@Override
public ChangeNotesState deserialize(byte[] in) {
ChangeNotesStateProto proto = Protos.parseUnchecked(ChangeNotesStateProto.parser(), in);
Change.Id changeId = Change.id(proto.getChangeId());
ChangeNotesState.Builder b =
builder()
.metaId(ObjectIdConverter.create().fromByteString(proto.getMetaId()))
.changeId(changeId)
.columns(toChangeColumns(changeId, proto.getColumns()))
.serverId(proto.getHasServerId() ? proto.getServerId() : null)
.hashtags(proto.getHashtagList())
.customKeyedValues(proto.getCustomKeyedValuesMap().entrySet())
.patchSets(
proto.getPatchSetList().stream()
.map(msg -> PatchSetProtoConverter.INSTANCE.fromProto(msg))
.map(ps -> Maps.immutableEntry(ps.id(), ps))
.collect(toImmutableList()))
.approvals(
proto.getApprovalList().stream()
.map(msg -> PatchSetApprovalProtoConverter.INSTANCE.fromProto(msg))
.map(a -> Maps.immutableEntry(a.patchSetId(), 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).collect(toImmutableList()))
.reviewerUpdates(toReviewerStatusUpdateList(proto.getReviewerUpdateList()))
.attentionSet(toAttentionSetUpdates(proto.getAttentionSetUpdateList()))
.allAttentionSetUpdates(
toAllAttentionSetUpdates(proto.getAllAttentionSetUpdateList()))
.submitRecords(
proto.getSubmitRecordList().stream()
.map(r -> GSON.fromJson(r, StoredSubmitRecord.class).toSubmitRecord())
.collect(toImmutableList()))
.changeMessages(
proto.getChangeMessageList().stream()
.map(msg -> ChangeMessageProtoConverter.INSTANCE.fromProto(msg))
.collect(toImmutableList()))
.publishedComments(
proto.getPublishedCommentList().stream()
.map(r -> GSON.fromJson(r, HumanComment.class))
.collect(toImmutableListMultimap(HumanComment::getCommitId, c -> c)))
.submitRequirementsResult(
proto.getSubmitRequirementResultList().stream()
.map(sr -> SubmitRequirementProtoConverter.INSTANCE.fromProto(sr))
.collect(toImmutableList()))
.updateCount(proto.getUpdateCount())
.mergedOn(
proto.getHasMergedOn() ? Instant.ofEpochMilli(proto.getMergedOnMillis()) : null);
return b.build();
}
private static ChangeColumns toChangeColumns(Change.Id changeId, ChangeColumnsProto proto) {
ChangeColumns.Builder b =
ChangeColumns.builder()
.changeKey(Change.key(proto.getChangeKey()))
.createdOn(Instant.ofEpochMilli(proto.getCreatedOnMillis()))
.lastUpdatedOn(Instant.ofEpochMilli(proto.getLastUpdatedOnMillis()))
.owner(Account.id(proto.getOwner()))
.branch(proto.getBranch());
if (proto.getHasCurrentPatchSetId()) {
b.currentPatchSetId(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.getHasStatus()) {
b.status(STATUS_CONVERTER.convert(proto.getStatus()));
}
b.isPrivate(proto.getIsPrivate())
.workInProgress(proto.getWorkInProgress())
.reviewStarted(proto.getReviewStarted());
if (proto.getHasRevertOf()) {
b.revertOf(Change.id(proto.getRevertOf()));
}
if (proto.getHasCherryPickOf()) {
b.cherryPickOf(PatchSet.Id.parse(proto.getCherryPickOf()));
}
return b.build();
}
private static ReviewerSet toReviewerSet(List<ReviewerSetEntryProto> protos) {
ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Instant> b =
ImmutableTable.builder();
for (ReviewerSetEntryProto e : protos) {
b.put(
REVIEWER_STATE_CONVERTER.convert(e.getState()),
Account.id(e.getAccountId()),
Instant.ofEpochMilli(e.getTimestampMillis()));
}
return ReviewerSet.fromTable(b.build());
}
private static ReviewerByEmailSet toReviewerByEmailSet(
List<ReviewerByEmailSetEntryProto> protos) {
ImmutableTable.Builder<ReviewerStateInternal, Address, Instant> b = ImmutableTable.builder();
for (ReviewerByEmailSetEntryProto e : protos) {
b.put(
REVIEWER_STATE_CONVERTER.convert(e.getState()),
Address.parse(e.getAddress()),
Instant.ofEpochMilli(e.getTimestampMillis()));
}
return ReviewerByEmailSet.fromTable(b.build());
}
private static ImmutableList<ReviewerStatusUpdate> toReviewerStatusUpdateList(
List<ReviewerStatusUpdateProto> protos) {
ImmutableList.Builder<ReviewerStatusUpdate> b = ImmutableList.builder();
for (ReviewerStatusUpdateProto proto : protos) {
if (proto.getHasReviewerByEmail()) {
b.add(
ReviewerStatusUpdate.createForReviewerByEmail(
Instant.ofEpochMilli(proto.getTimestampMillis()),
Account.id(proto.getUpdatedBy()),
Address.parse(proto.getReviewerByEmail()),
REVIEWER_STATE_CONVERTER.convert(proto.getState())));
} else {
// If the "has_reviewer_by_email" field is not set, then either the "has_reviewer" field
// is true and the "reviewer" field is populated, or the proto was created before the
// "has_reviewer", "has_reviewer_by_email" and "reviewer_by_email" fields have been added
// and the "reviewer" field is always populated. This means by not checking that
// "proto.getHasReviewer()" is true here we allow the new code to read old protos.
b.add(
ReviewerStatusUpdate.createForReviewer(
Instant.ofEpochMilli(proto.getTimestampMillis()),
Account.id(proto.getUpdatedBy()),
Account.id(proto.getReviewer()),
REVIEWER_STATE_CONVERTER.convert(proto.getState())));
}
}
return b.build();
}
private static ImmutableSet<AttentionSetUpdate> toAttentionSetUpdates(
List<AttentionSetUpdateProto> protos) {
ImmutableSet.Builder<AttentionSetUpdate> b = ImmutableSet.builder();
for (AttentionSetUpdateProto proto : protos) {
b.add(
AttentionSetUpdate.createFromRead(
Instant.ofEpochMilli(proto.getTimestampMillis()),
Account.id(proto.getAccount()),
AttentionSetUpdate.Operation.valueOf(proto.getOperation()),
proto.getReason()));
}
return b.build();
}
private static ImmutableList<AttentionSetUpdate> toAllAttentionSetUpdates(
List<AttentionSetUpdateProto> protos) {
ImmutableList.Builder<AttentionSetUpdate> b = ImmutableList.builder();
for (AttentionSetUpdateProto proto : protos) {
b.add(
AttentionSetUpdate.createFromRead(
Instant.ofEpochMilli(proto.getTimestampMillis()),
Account.id(proto.getAccount()),
AttentionSetUpdate.Operation.valueOf(proto.getOperation()),
proto.getReason()));
}
return b.build();
}
}
}