// 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.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.auto.value.AutoValue; 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.ListMultimap; import com.google.gerrit.common.Nullable; import com.google.gerrit.common.data.SubmitRecord; import com.google.gerrit.reviewdb.client.Account; import com.google.gerrit.reviewdb.client.Branch; import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.ChangeMessage; import com.google.gerrit.reviewdb.client.Comment; import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSetApproval; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.RevId; import com.google.gerrit.server.ReviewerByEmailSet; import com.google.gerrit.server.ReviewerSet; import com.google.gerrit.server.ReviewerStatusUpdate; import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; 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.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>Note that {@link ChangeNotes} contains more than just a single {@code ChangeNoteState}, such * as per-draft information, so that class is not cached directly. */ @AutoValue public abstract class ChangeNotesState { static ChangeNotesState empty(Change change) { return new AutoValue_ChangeNotesState( null, change.getId(), null, ImmutableSet.of(), ImmutableSet.of(), ImmutableList.of(), ImmutableList.of(), ReviewerSet.empty(), ReviewerByEmailSet.empty(), ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), ImmutableList.of(), ImmutableListMultimap.of(), ImmutableListMultimap.of(), null, null, null); } static ChangeNotesState create( @Nullable ObjectId metaId, Change.Id changeId, Change.Key changeKey, Timestamp createdOn, Timestamp lastUpdatedOn, Account.Id owner, String branch, @Nullable PatchSet.Id currentPatchSetId, String subject, @Nullable String topic, @Nullable String originalSubject, @Nullable String submissionId, @Nullable Account.Id assignee, @Nullable Change.Status status, @Nullable Set<Account.Id> pastAssignees, @Nullable Set<String> hashtags, Map<PatchSet.Id, PatchSet> patchSets, ListMultimap<PatchSet.Id, PatchSetApproval> approvals, ReviewerSet reviewers, ReviewerByEmailSet reviewersByEmail, List<Account.Id> allPastReviewers, List<ReviewerStatusUpdate> reviewerUpdates, List<SubmitRecord> submitRecords, List<ChangeMessage> allChangeMessages, ListMultimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet, ListMultimap<RevId, Comment> publishedComments, @Nullable Timestamp readOnlyUntil, @Nullable Boolean isPrivate, @Nullable Boolean workInProgress) { if (hashtags == null) { hashtags = ImmutableSet.of(); } return new AutoValue_ChangeNotesState( metaId, changeId, new AutoValue_ChangeNotesState_ChangeColumns( changeKey, createdOn, lastUpdatedOn, owner, branch, currentPatchSetId, subject, topic, originalSubject, submissionId, assignee, status, isPrivate, workInProgress), ImmutableSet.copyOf(pastAssignees), ImmutableSet.copyOf(hashtags), ImmutableList.copyOf(patchSets.entrySet()), ImmutableList.copyOf(approvals.entries()), reviewers, reviewersByEmail, ImmutableList.copyOf(allPastReviewers), ImmutableList.copyOf(reviewerUpdates), ImmutableList.copyOf(submitRecords), ImmutableList.copyOf(allChangeMessages), ImmutableListMultimap.copyOf(changeMessagesByPatchSet), ImmutableListMultimap.copyOf(publishedComments), readOnlyUntil, isPrivate, workInProgress); } /** * Subset of Change columns that can be represented in NoteDb. * * <p>Notable exceptions include rowVersion and noteDbState, which are only make sense when read * from NoteDb, so they cannot be cached. * * <p>Fields are in listed column order. */ @AutoValue abstract static class ChangeColumns { abstract Change.Key changeKey(); abstract Timestamp createdOn(); abstract Timestamp lastUpdatedOn(); abstract Account.Id owner(); // Project not included, as it's not stored anywhere in the meta ref. abstract String branch(); @Nullable abstract PatchSet.Id currentPatchSetId(); abstract String subject(); @Nullable abstract String topic(); @Nullable abstract String originalSubject(); @Nullable abstract String submissionId(); @Nullable abstract Account.Id assignee(); // TODO(dborowitz): Use a sensible default other than null @Nullable abstract Change.Status status(); @Nullable abstract Boolean isPrivate(); @Nullable abstract Boolean isWorkInProgress(); } // 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<Account.Id> pastAssignees(); abstract ImmutableSet<String> hashtags(); abstract ImmutableList<Map.Entry<PatchSet.Id, PatchSet>> patchSets(); abstract ImmutableList<Map.Entry<PatchSet.Id, PatchSetApproval>> approvals(); abstract ReviewerSet reviewers(); abstract ReviewerByEmailSet reviewersByEmail(); abstract ImmutableList<Account.Id> allPastReviewers(); abstract ImmutableList<ReviewerStatusUpdate> reviewerUpdates(); abstract ImmutableList<SubmitRecord> submitRecords(); abstract ImmutableList<ChangeMessage> allChangeMessages(); abstract ImmutableListMultimap<PatchSet.Id, ChangeMessage> changeMessagesByPatchSet(); abstract ImmutableListMultimap<RevId, Comment> publishedComments(); @Nullable abstract Timestamp readOnlyUntil(); @Nullable abstract Boolean isPrivate(); @Nullable abstract Boolean isWorkInProgress(); Change newChange(Project.NameKey project) { ChangeColumns c = checkNotNull(columns(), "columns are required"); Change change = new Change( c.changeKey(), changeId(), c.owner(), new Branch.NameKey(project, c.branch()), c.createdOn()); copyNonConstructorColumnsTo(change); change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE); return change; } void copyColumnsTo(Change change) throws IOException { ChangeColumns c = columns(); checkState( c != null && metaId() != null, "missing columns or metaId in ChangeNotesState; is NoteDb enabled? %s", this); checkMetaId(change); change.setKey(c.changeKey()); change.setOwner(c.owner()); change.setDest(new Branch.NameKey(change.getProject(), c.branch())); change.setCreatedOn(c.createdOn()); copyNonConstructorColumnsTo(change); } private void checkMetaId(Change change) throws IOException { NoteDbChangeState state = NoteDbChangeState.parse(change); if (state == null) { return; // Can happen during small NoteDb tests. } else if (state.getPrimaryStorage() == PrimaryStorage.NOTE_DB) { return; } checkState(state.getRefState().isPresent(), "expected RefState: %s", state); ObjectId idFromState = state.getRefState().get().changeMetaId(); if (!idFromState.equals(metaId())) { throw new IOException( "cannot copy ChangeNotesState into Change " + changeId() + "; this ChangeNotesState was created from " + metaId() + ", but change requires state " + idFromState); } } private void copyNonConstructorColumnsTo(Change change) { ChangeColumns c = checkNotNull(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.setAssignee(c.assignee()); change.setPrivate(c.isPrivate() == null ? false : c.isPrivate()); change.setWorkInProgress(c.isWorkInProgress() == null ? false : c.isWorkInProgress()); 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(); } } }