| // Copyright (C) 2013 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.checkNotNull; |
| import static com.google.common.base.Preconditions.checkState; |
| import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef; |
| import static com.google.gerrit.server.notedb.NoteDbTable.CHANGES; |
| import static java.util.Comparator.comparing; |
| |
| import com.google.auto.value.AutoValue; |
| import com.google.common.annotations.VisibleForTesting; |
| 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.ImmutableSortedSet; |
| import com.google.common.collect.Iterators; |
| import com.google.common.collect.ListMultimap; |
| import com.google.common.collect.MultimapBuilder; |
| import com.google.common.collect.Multimaps; |
| import com.google.common.collect.Ordering; |
| import com.google.common.collect.Sets; |
| import com.google.common.collect.Sets.SetView; |
| import com.google.common.collect.Streams; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.common.data.SubmitRecord; |
| import com.google.gerrit.metrics.Timer1; |
| 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.RefNames; |
| import com.google.gerrit.reviewdb.client.RevId; |
| import com.google.gerrit.reviewdb.client.RobotComment; |
| import com.google.gerrit.reviewdb.server.ReviewDb; |
| import com.google.gerrit.reviewdb.server.ReviewDbUtil; |
| import com.google.gerrit.server.ReviewerByEmailSet; |
| import com.google.gerrit.server.ReviewerSet; |
| import com.google.gerrit.server.ReviewerStatusUpdate; |
| import com.google.gerrit.server.git.RefCache; |
| import com.google.gerrit.server.git.RepoRefCache; |
| import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; |
| import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder; |
| import com.google.gerrit.server.project.NoSuchChangeException; |
| import com.google.gerrit.server.project.ProjectCache; |
| import com.google.gerrit.server.query.change.ChangeData; |
| import com.google.gerrit.server.query.change.InternalChangeQuery; |
| import com.google.gwtorm.server.OrmException; |
| import com.google.inject.Inject; |
| import com.google.inject.Provider; |
| import com.google.inject.Singleton; |
| import java.io.IOException; |
| import java.sql.Timestamp; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.concurrent.TimeUnit; |
| import java.util.function.Predicate; |
| import java.util.stream.Stream; |
| import org.eclipse.jgit.errors.ConfigInvalidException; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Ref; |
| import org.eclipse.jgit.lib.Repository; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** View of a single {@link Change} based on the log of its notes branch. */ |
| public class ChangeNotes extends AbstractChangeNotes<ChangeNotes> { |
| private static final Logger log = LoggerFactory.getLogger(ChangeNotes.class); |
| |
| static final Ordering<PatchSetApproval> PSA_BY_TIME = |
| Ordering.from(comparing(PatchSetApproval::getGranted)); |
| |
| public static final Ordering<ChangeMessage> MESSAGE_BY_TIME = |
| Ordering.from(comparing(ChangeMessage::getWrittenOn)); |
| |
| public static ConfigInvalidException parseException( |
| Change.Id changeId, String fmt, Object... args) { |
| return new ConfigInvalidException("Change " + changeId + ": " + String.format(fmt, args)); |
| } |
| |
| @Nullable |
| public static Change readOneReviewDbChange(ReviewDb db, Change.Id id) throws OrmException { |
| return ReviewDbUtil.unwrapDb(db).changes().get(id); |
| } |
| |
| @Singleton |
| public static class Factory { |
| private final Args args; |
| private final Provider<InternalChangeQuery> queryProvider; |
| private final ProjectCache projectCache; |
| |
| @VisibleForTesting |
| @Inject |
| public Factory( |
| Args args, Provider<InternalChangeQuery> queryProvider, ProjectCache projectCache) { |
| this.args = args; |
| this.queryProvider = queryProvider; |
| this.projectCache = projectCache; |
| } |
| |
| public ChangeNotes createChecked(ReviewDb db, Change c) throws OrmException { |
| return createChecked(db, c.getProject(), c.getId()); |
| } |
| |
| public ChangeNotes createChecked(ReviewDb db, Project.NameKey project, Change.Id changeId) |
| throws OrmException { |
| Change change = readOneReviewDbChange(db, changeId); |
| if (change == null) { |
| if (!args.migration.readChanges()) { |
| throw new NoSuchChangeException(changeId); |
| } |
| // Change isn't in ReviewDb, but its primary storage might be in NoteDb. |
| // Prepopulate the change exists with proper noteDbState field. |
| change = newNoteDbOnlyChange(project, changeId); |
| } else if (!change.getProject().equals(project)) { |
| throw new NoSuchChangeException(changeId); |
| } |
| return new ChangeNotes(args, change).load(); |
| } |
| |
| public ChangeNotes createChecked(Change.Id changeId) throws OrmException { |
| InternalChangeQuery query = queryProvider.get().noFields(); |
| List<ChangeData> changes = query.byLegacyChangeId(changeId); |
| if (changes.isEmpty()) { |
| throw new NoSuchChangeException(changeId); |
| } |
| if (changes.size() != 1) { |
| log.error(String.format("Multiple changes found for %d", changeId.get())); |
| throw new NoSuchChangeException(changeId); |
| } |
| return changes.get(0).notes(); |
| } |
| |
| public static Change newNoteDbOnlyChange(Project.NameKey project, Change.Id changeId) { |
| Change change = |
| new Change( |
| null, changeId, null, new Branch.NameKey(project, "INVALID_NOTE_DB_ONLY"), null); |
| change.setNoteDbState(NoteDbChangeState.NOTE_DB_PRIMARY_STATE); |
| return change; |
| } |
| |
| private Change loadChangeFromDb(ReviewDb db, Project.NameKey project, Change.Id changeId) |
| throws OrmException { |
| checkArgument(project != null, "project is required"); |
| Change change = readOneReviewDbChange(db, changeId); |
| |
| if (change == null) { |
| if (args.migration.readChanges()) { |
| return newNoteDbOnlyChange(project, changeId); |
| } |
| throw new NoSuchChangeException(changeId); |
| } |
| checkArgument( |
| change.getProject().equals(project), |
| "passed project %s when creating ChangeNotes for %s, but actual project is %s", |
| project, |
| changeId, |
| change.getProject()); |
| return change; |
| } |
| |
| public ChangeNotes create(ReviewDb db, Project.NameKey project, Change.Id changeId) |
| throws OrmException { |
| return new ChangeNotes(args, loadChangeFromDb(db, project, changeId)).load(); |
| } |
| |
| public ChangeNotes createWithAutoRebuildingDisabled( |
| ReviewDb db, Project.NameKey project, Change.Id changeId) throws OrmException { |
| return new ChangeNotes(args, loadChangeFromDb(db, project, changeId), true, false, null) |
| .load(); |
| } |
| |
| /** |
| * Create change notes for a change that was loaded from index. This method should only be used |
| * when database access is harmful and potentially stale data from the index is acceptable. |
| * |
| * @param change change loaded from secondary index |
| * @return change notes |
| */ |
| public ChangeNotes createFromIndexedChange(Change change) { |
| return new ChangeNotes(args, change); |
| } |
| |
| public ChangeNotes createForBatchUpdate(Change change, boolean shouldExist) |
| throws OrmException { |
| return new ChangeNotes(args, change, shouldExist, false, null).load(); |
| } |
| |
| public ChangeNotes createWithAutoRebuildingDisabled(Change change, RefCache refs) |
| throws OrmException { |
| return new ChangeNotes(args, change, true, false, refs).load(); |
| } |
| |
| // TODO(ekempin): Remove when database backend is deleted |
| /** |
| * Instantiate ChangeNotes for a change that has been loaded by a batch read from the database. |
| */ |
| private ChangeNotes createFromChangeOnlyWhenNoteDbDisabled(Change change) throws OrmException { |
| checkState( |
| !args.migration.readChanges(), |
| "do not call createFromChangeWhenNoteDbDisabled when NoteDb is enabled"); |
| return new ChangeNotes(args, change).load(); |
| } |
| |
| public List<ChangeNotes> create(ReviewDb db, Collection<Change.Id> changeIds) |
| throws OrmException { |
| List<ChangeNotes> notes = new ArrayList<>(); |
| if (args.migration.readChanges()) { |
| for (Change.Id changeId : changeIds) { |
| try { |
| notes.add(createChecked(changeId)); |
| } catch (NoSuchChangeException e) { |
| // Ignore missing changes to match Access#get(Iterable) behavior. |
| } |
| } |
| return notes; |
| } |
| |
| for (Change c : ReviewDbUtil.unwrapDb(db).changes().get(changeIds)) { |
| notes.add(createFromChangeOnlyWhenNoteDbDisabled(c)); |
| } |
| return notes; |
| } |
| |
| public List<ChangeNotes> create( |
| ReviewDb db, |
| Project.NameKey project, |
| Collection<Change.Id> changeIds, |
| Predicate<ChangeNotes> predicate) |
| throws OrmException { |
| List<ChangeNotes> notes = new ArrayList<>(); |
| if (args.migration.readChanges()) { |
| for (Change.Id cid : changeIds) { |
| try { |
| ChangeNotes cn = create(db, project, cid); |
| if (cn.getChange() != null && predicate.test(cn)) { |
| notes.add(cn); |
| } |
| } catch (NoSuchChangeException e) { |
| // Match ReviewDb behavior, returning not found; maybe the caller learned about it from |
| // a dangling patch set ref or something. |
| continue; |
| } |
| } |
| return notes; |
| } |
| |
| for (Change c : ReviewDbUtil.unwrapDb(db).changes().get(changeIds)) { |
| if (c != null && project.equals(c.getDest().getParentKey())) { |
| ChangeNotes cn = createFromChangeOnlyWhenNoteDbDisabled(c); |
| if (predicate.test(cn)) { |
| notes.add(cn); |
| } |
| } |
| } |
| return notes; |
| } |
| |
| public ListMultimap<Project.NameKey, ChangeNotes> create( |
| ReviewDb db, Predicate<ChangeNotes> predicate) throws IOException, OrmException { |
| ListMultimap<Project.NameKey, ChangeNotes> m = |
| MultimapBuilder.hashKeys().arrayListValues().build(); |
| if (args.migration.readChanges()) { |
| for (Project.NameKey project : projectCache.all()) { |
| try (Repository repo = args.repoManager.openRepository(project)) { |
| scanNoteDb(repo, db, project) |
| .filter(r -> !r.error().isPresent()) |
| .map(ChangeNotesResult::notes) |
| .filter(predicate) |
| .forEach(n -> m.put(n.getProjectName(), n)); |
| } |
| } |
| } else { |
| for (Change change : ReviewDbUtil.unwrapDb(db).changes().all()) { |
| ChangeNotes notes = createFromChangeOnlyWhenNoteDbDisabled(change); |
| if (predicate.test(notes)) { |
| m.put(change.getProject(), notes); |
| } |
| } |
| } |
| return ImmutableListMultimap.copyOf(m); |
| } |
| |
| public Stream<ChangeNotesResult> scan(Repository repo, ReviewDb db, Project.NameKey project) |
| throws IOException { |
| return args.migration.readChanges() ? scanNoteDb(repo, db, project) : scanReviewDb(repo, db); |
| } |
| |
| private Stream<ChangeNotesResult> scanReviewDb(Repository repo, ReviewDb db) |
| throws IOException { |
| // Scan IDs that might exist in ReviewDb, assuming that each change has at least one patch set |
| // ref. Not all changes might exist: some patch set refs might have been written where the |
| // corresponding ReviewDb write failed. These will be silently filtered out by the batch get |
| // call below, which is intended. |
| Set<Change.Id> ids = scanChangeIds(repo).fromPatchSetRefs(); |
| |
| // A batch size of N may overload get(Iterable), so use something smaller, but still >1. |
| return Streams.stream(Iterators.partition(ids.iterator(), 30)) |
| .flatMap( |
| batch -> { |
| try { |
| return Streams.stream(ReviewDbUtil.unwrapDb(db).changes().get(batch)) |
| .map(this::toResult) |
| .filter(Objects::nonNull); |
| } catch (OrmException e) { |
| // Return this error for each Id in the input batch. |
| return batch.stream().map(id -> ChangeNotesResult.error(id, e)); |
| } |
| }); |
| } |
| |
| private Stream<ChangeNotesResult> scanNoteDb( |
| Repository repo, ReviewDb db, Project.NameKey project) throws IOException { |
| ScanResult sr = scanChangeIds(repo); |
| PrimaryStorage defaultStorage = args.migration.changePrimaryStorage(); |
| |
| return sr.all() |
| .stream() |
| .map(id -> scanOneNoteDbChange(db, project, sr, defaultStorage, id)) |
| .filter(Objects::nonNull); |
| } |
| |
| private ChangeNotesResult scanOneNoteDbChange( |
| ReviewDb db, |
| Project.NameKey project, |
| ScanResult sr, |
| PrimaryStorage defaultStorage, |
| Change.Id id) { |
| Change change; |
| try { |
| change = readOneReviewDbChange(db, id); |
| } catch (OrmException e) { |
| return ChangeNotesResult.error(id, e); |
| } |
| |
| if (change == null) { |
| if (!sr.fromMetaRefs().contains(id)) { |
| // Stray patch set refs can happen due to normal error conditions, e.g. failed |
| // push processing, so aren't worth even a warning. |
| return null; |
| } |
| if (defaultStorage == PrimaryStorage.REVIEW_DB) { |
| // If changes should exist in ReviewDb, it's worth warning about a meta ref with |
| // no corresponding ReviewDb data. |
| log.warn("skipping change {} found in project {} but not in ReviewDb", id, project); |
| return null; |
| } |
| // TODO(dborowitz): See discussion in NoteDbBatchUpdate#newChangeContext. |
| change = ChangeNotes.Factory.newNoteDbOnlyChange(project, id); |
| } else if (!change.getProject().equals(project)) { |
| log.error( |
| "skipping change {} found in project {} because ReviewDb change has" + " project {}", |
| id, |
| project, |
| change.getProject()); |
| return null; |
| } |
| log.debug("adding change {} found in project {}", id, project); |
| return toResult(change); |
| } |
| |
| @Nullable |
| private ChangeNotesResult toResult(Change rawChangeFromReviewDbOrNoteDb) { |
| ChangeNotes n = new ChangeNotes(args, rawChangeFromReviewDbOrNoteDb); |
| try { |
| n.load(); |
| } catch (OrmException e) { |
| return ChangeNotesResult.error(n.getChangeId(), e); |
| } |
| return ChangeNotesResult.notes(n); |
| } |
| |
| /** Result of {@link #scan(Repository, ReviewDb, Project.NameKey)}. */ |
| @AutoValue |
| public abstract static class ChangeNotesResult { |
| static ChangeNotesResult error(Change.Id id, OrmException e) { |
| return new AutoValue_ChangeNotes_Factory_ChangeNotesResult(id, Optional.of(e), null); |
| } |
| |
| static ChangeNotesResult notes(ChangeNotes notes) { |
| return new AutoValue_ChangeNotes_Factory_ChangeNotesResult( |
| notes.getChangeId(), Optional.empty(), notes); |
| } |
| |
| /** Change ID that was scanned. */ |
| public abstract Change.Id id(); |
| |
| /** Error encountered while loading this change, if any. */ |
| public abstract Optional<OrmException> error(); |
| |
| /** |
| * Notes loaded for this change. |
| * |
| * @return notes. |
| * @throws IllegalStateException if there was an error loading the change; callers must check |
| * that {@link #error()} is absent before attempting to look up the notes. |
| */ |
| public ChangeNotes notes() { |
| checkState(maybeNotes() != null, "no ChangeNotes loaded; check error().isPresent() first"); |
| return maybeNotes(); |
| } |
| |
| @Nullable |
| abstract ChangeNotes maybeNotes(); |
| } |
| |
| @AutoValue |
| abstract static class ScanResult { |
| abstract ImmutableSet<Change.Id> fromPatchSetRefs(); |
| |
| abstract ImmutableSet<Change.Id> fromMetaRefs(); |
| |
| SetView<Change.Id> all() { |
| return Sets.union(fromPatchSetRefs(), fromMetaRefs()); |
| } |
| } |
| |
| private static ScanResult scanChangeIds(Repository repo) throws IOException { |
| ImmutableSet.Builder<Change.Id> fromPs = ImmutableSet.builder(); |
| ImmutableSet.Builder<Change.Id> fromMeta = ImmutableSet.builder(); |
| for (Ref r : repo.getRefDatabase().getRefs(RefNames.REFS_CHANGES).values()) { |
| Change.Id id = Change.Id.fromRef(r.getName()); |
| if (id != null) { |
| (r.getName().endsWith(RefNames.META_SUFFIX) ? fromMeta : fromPs).add(id); |
| } |
| } |
| return new AutoValue_ChangeNotes_Factory_ScanResult(fromPs.build(), fromMeta.build()); |
| } |
| } |
| |
| private final boolean shouldExist; |
| private final RefCache refs; |
| |
| private Change change; |
| private ChangeNotesState state; |
| |
| // Parsed note map state, used by ChangeUpdate to make in-place editing of |
| // notes easier. |
| RevisionNoteMap<ChangeRevisionNote> revisionNoteMap; |
| |
| private NoteDbUpdateManager.Result rebuildResult; |
| private DraftCommentNotes draftCommentNotes; |
| private RobotCommentNotes robotCommentNotes; |
| |
| // Lazy defensive copies of mutable ReviewDb types, to avoid polluting the |
| // ChangeNotesCache from handlers. |
| private ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets; |
| private ImmutableListMultimap<PatchSet.Id, PatchSetApproval> approvals; |
| private ImmutableSet<Comment.Key> commentKeys; |
| |
| @VisibleForTesting |
| public ChangeNotes(Args args, Change change) { |
| this(args, change, true, true, null); |
| } |
| |
| private ChangeNotes( |
| Args args, Change change, boolean shouldExist, boolean autoRebuild, @Nullable RefCache refs) { |
| super(args, change.getId(), PrimaryStorage.of(change), autoRebuild); |
| this.change = new Change(change); |
| this.shouldExist = shouldExist; |
| this.refs = refs; |
| } |
| |
| public Change getChange() { |
| return change; |
| } |
| |
| public ObjectId getMetaId() { |
| return state.metaId(); |
| } |
| |
| public ImmutableSortedMap<PatchSet.Id, PatchSet> getPatchSets() { |
| if (patchSets == null) { |
| ImmutableSortedMap.Builder<PatchSet.Id, PatchSet> b = |
| ImmutableSortedMap.orderedBy(comparing(PatchSet.Id::get)); |
| for (Map.Entry<PatchSet.Id, PatchSet> e : state.patchSets()) { |
| b.put(e.getKey(), new PatchSet(e.getValue())); |
| } |
| patchSets = b.build(); |
| } |
| return patchSets; |
| } |
| |
| public ImmutableListMultimap<PatchSet.Id, PatchSetApproval> getApprovals() { |
| if (approvals == null) { |
| ImmutableListMultimap.Builder<PatchSet.Id, PatchSetApproval> b = |
| ImmutableListMultimap.builder(); |
| for (Map.Entry<PatchSet.Id, PatchSetApproval> e : state.approvals()) { |
| b.put(e.getKey(), new PatchSetApproval(e.getValue())); |
| } |
| approvals = b.build(); |
| } |
| return approvals; |
| } |
| |
| public ReviewerSet getReviewers() { |
| return state.reviewers(); |
| } |
| |
| /** @return reviewers that do not currently have a Gerrit account and were added by email. */ |
| public ReviewerByEmailSet getReviewersByEmail() { |
| return state.reviewersByEmail(); |
| } |
| |
| /** @return reviewers that were modified during this change's current WIP phase. */ |
| public ReviewerSet getPendingReviewers() { |
| return state.pendingReviewers(); |
| } |
| |
| /** @return reviewers by email that were modified during this change's current WIP phase. */ |
| public ReviewerByEmailSet getPendingReviewersByEmail() { |
| return state.pendingReviewersByEmail(); |
| } |
| |
| public ImmutableList<ReviewerStatusUpdate> getReviewerUpdates() { |
| return state.reviewerUpdates(); |
| } |
| |
| /** @return an ImmutableSet of Account.Ids of all users that have been assigned to this change. */ |
| public ImmutableSet<Account.Id> getPastAssignees() { |
| return state.pastAssignees(); |
| } |
| |
| /** @return a ImmutableSet of all hashtags for this change sorted in alphabetical order. */ |
| public ImmutableSet<String> getHashtags() { |
| return ImmutableSortedSet.copyOf(state.hashtags()); |
| } |
| |
| /** @return a list of all users who have ever been a reviewer on this change. */ |
| public ImmutableList<Account.Id> getAllPastReviewers() { |
| return state.allPastReviewers(); |
| } |
| |
| /** |
| * @return submit records stored during the most recent submit; only for changes that were |
| * actually submitted. |
| */ |
| public ImmutableList<SubmitRecord> getSubmitRecords() { |
| return state.submitRecords(); |
| } |
| |
| /** @return all change messages, in chronological order, oldest first. */ |
| public ImmutableList<ChangeMessage> getChangeMessages() { |
| return state.allChangeMessages(); |
| } |
| |
| /** @return change messages by patch set, in chronological order, oldest first. */ |
| public ImmutableListMultimap<PatchSet.Id, ChangeMessage> getChangeMessagesByPatchSet() { |
| return state.changeMessagesByPatchSet(); |
| } |
| |
| /** @return inline comments on each revision. */ |
| public ImmutableListMultimap<RevId, Comment> getComments() { |
| return state.publishedComments(); |
| } |
| |
| public ImmutableSet<Comment.Key> getCommentKeys() { |
| if (commentKeys == null) { |
| ImmutableSet.Builder<Comment.Key> b = ImmutableSet.builder(); |
| for (Comment c : getComments().values()) { |
| b.add(new Comment.Key(c.key)); |
| } |
| commentKeys = b.build(); |
| } |
| return commentKeys; |
| } |
| |
| public ImmutableListMultimap<RevId, Comment> getDraftComments(Account.Id author) |
| throws OrmException { |
| return getDraftComments(author, null); |
| } |
| |
| public ImmutableListMultimap<RevId, Comment> getDraftComments( |
| Account.Id author, @Nullable Ref ref) throws OrmException { |
| loadDraftComments(author, ref); |
| // Filter out any zombie draft comments. These are drafts that are also in |
| // the published map, and arise when the update to All-Users to delete them |
| // during the publish operation failed. |
| return ImmutableListMultimap.copyOf( |
| Multimaps.filterEntries( |
| draftCommentNotes.getComments(), e -> !getCommentKeys().contains(e.getValue().key))); |
| } |
| |
| public ImmutableListMultimap<RevId, RobotComment> getRobotComments() throws OrmException { |
| loadRobotComments(); |
| return robotCommentNotes.getComments(); |
| } |
| |
| /** |
| * If draft comments have already been loaded for this author, then they will not be reloaded. |
| * However, this method will load the comments if no draft comments have been loaded or if the |
| * caller would like the drafts for another author. |
| */ |
| private void loadDraftComments(Account.Id author, @Nullable Ref ref) throws OrmException { |
| if (draftCommentNotes == null || !author.equals(draftCommentNotes.getAuthor()) || ref != null) { |
| draftCommentNotes = |
| new DraftCommentNotes(args, change, author, autoRebuild, rebuildResult, ref); |
| draftCommentNotes.load(); |
| } |
| } |
| |
| private void loadRobotComments() throws OrmException { |
| if (robotCommentNotes == null) { |
| robotCommentNotes = new RobotCommentNotes(args, change); |
| robotCommentNotes.load(); |
| } |
| } |
| |
| @VisibleForTesting |
| DraftCommentNotes getDraftCommentNotes() { |
| return draftCommentNotes; |
| } |
| |
| public RobotCommentNotes getRobotCommentNotes() { |
| return robotCommentNotes; |
| } |
| |
| public boolean containsComment(Comment c) throws OrmException { |
| if (containsCommentPublished(c)) { |
| return true; |
| } |
| loadDraftComments(c.author.getId(), null); |
| return draftCommentNotes.containsComment(c); |
| } |
| |
| public boolean containsCommentPublished(Comment c) { |
| for (Comment l : getComments().values()) { |
| if (c.key.equals(l.key)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public String getRefName() { |
| return changeMetaRef(getChangeId()); |
| } |
| |
| public PatchSet getCurrentPatchSet() { |
| PatchSet.Id psId = change.currentPatchSetId(); |
| return checkNotNull(getPatchSets().get(psId), "missing current patch set %s", psId.get()); |
| } |
| |
| @VisibleForTesting |
| public Timestamp getReadOnlyUntil() { |
| return state.readOnlyUntil(); |
| } |
| |
| public boolean isPrivate() { |
| if (state.isPrivate() == null) { |
| return false; |
| } |
| return state.isPrivate(); |
| } |
| |
| public boolean isWorkInProgress() { |
| if (state.isWorkInProgress() == null) { |
| return false; |
| } |
| return state.isWorkInProgress(); |
| } |
| |
| public Change.Id getRevertOf() { |
| return state.revertOf(); |
| } |
| |
| public boolean hasReviewStarted() { |
| return state.hasReviewStarted(); |
| } |
| |
| @Override |
| protected void onLoad(LoadHandle handle) |
| throws NoSuchChangeException, IOException, ConfigInvalidException { |
| ObjectId rev = handle.id(); |
| if (rev == null) { |
| if (args.migration.readChanges() |
| && PrimaryStorage.of(change) == PrimaryStorage.NOTE_DB |
| && shouldExist) { |
| throw new NoSuchChangeException(getChangeId()); |
| } |
| loadDefaults(); |
| return; |
| } |
| |
| ChangeNotesCache.Value v = |
| args.cache.get().get(getProjectName(), getChangeId(), rev, handle.walk()); |
| state = v.state(); |
| state.copyColumnsTo(change); |
| revisionNoteMap = v.revisionNoteMap(); |
| } |
| |
| @Override |
| protected void loadDefaults() { |
| state = ChangeNotesState.empty(change); |
| } |
| |
| @Override |
| public Project.NameKey getProjectName() { |
| return change.getProject(); |
| } |
| |
| @Override |
| protected ObjectId readRef(Repository repo) throws IOException { |
| return refs != null ? refs.get(getRefName()).orElse(null) : super.readRef(repo); |
| } |
| |
| @Override |
| protected LoadHandle openHandle(Repository repo) throws NoSuchChangeException, IOException { |
| if (autoRebuild) { |
| NoteDbChangeState state = NoteDbChangeState.parse(change); |
| ObjectId id = readRef(repo); |
| if (id == null) { |
| if (state == null) { |
| return super.openHandle(repo, id); |
| } else if (shouldExist) { |
| throw new NoSuchChangeException(getChangeId()); |
| } |
| } |
| RefCache refs = this.refs != null ? this.refs : new RepoRefCache(repo); |
| if (!NoteDbChangeState.isChangeUpToDate(state, refs, getChangeId())) { |
| return rebuildAndOpen(repo, id); |
| } |
| } |
| return super.openHandle(repo); |
| } |
| |
| private LoadHandle rebuildAndOpen(Repository repo, ObjectId oldId) throws IOException { |
| Timer1.Context timer = args.metrics.autoRebuildLatency.start(CHANGES); |
| try { |
| Change.Id cid = getChangeId(); |
| ReviewDb db = args.db.get(); |
| ChangeRebuilder rebuilder = args.rebuilder.get(); |
| NoteDbUpdateManager.Result r; |
| try (NoteDbUpdateManager manager = rebuilder.stage(db, cid)) { |
| if (manager == null) { |
| return super.openHandle(repo, oldId); // May be null in tests. |
| } |
| manager.setRefLogMessage("Auto-rebuilding change"); |
| r = manager.stageAndApplyDelta(change); |
| try { |
| rebuilder.execute(db, cid, manager); |
| repo.scanForRepoChanges(); |
| } catch (OrmException | IOException e) { |
| // Rebuilding failed. Most likely cause is contention on one or more |
| // change refs; there are other types of errors that can happen during |
| // rebuilding, but generally speaking they should happen during stage(), |
| // not execute(). Assume that some other worker is going to successfully |
| // store the rebuilt state, which is deterministic given an input |
| // ChangeBundle. |
| // |
| // Parse notes from the staged result so we can return something useful |
| // to the caller instead of throwing. |
| log.debug("Rebuilding change {} failed: {}", getChangeId(), e.getMessage()); |
| args.metrics.autoRebuildFailureCount.increment(CHANGES); |
| rebuildResult = checkNotNull(r); |
| checkNotNull(r.newState()); |
| checkNotNull(r.staged()); |
| return LoadHandle.create( |
| ChangeNotesCommit.newStagedRevWalk(repo, r.staged().changeObjects()), |
| r.newState().getChangeMetaId()); |
| } |
| } |
| return LoadHandle.create(ChangeNotesCommit.newRevWalk(repo), r.newState().getChangeMetaId()); |
| } catch (NoSuchChangeException e) { |
| return super.openHandle(repo, oldId); |
| } catch (OrmException e) { |
| throw new IOException(e); |
| } finally { |
| log.debug( |
| "Rebuilt change {} in project {} in {} ms", |
| getChangeId(), |
| getProjectName(), |
| TimeUnit.MILLISECONDS.convert(timer.stop(), TimeUnit.NANOSECONDS)); |
| } |
| } |
| } |