diff options
Diffstat (limited to 'java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java')
-rw-r--r-- | java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java | 685 |
1 files changed, 685 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java b/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java new file mode 100644 index 0000000000..0d516e5b88 --- /dev/null +++ b/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java @@ -0,0 +1,685 @@ +// Copyright (C) 2014 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.rebuild; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Preconditions.checkState; +import static com.google.gerrit.reviewdb.client.RefNames.changeMetaRef; +import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_HASHTAGS; +import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.SECONDS; +import static java.util.stream.Collectors.toList; + +import com.google.common.base.Splitter; +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.Ordering; +import com.google.common.collect.Sets; +import com.google.common.collect.Table; +import com.google.common.primitives.Ints; +import com.google.gerrit.common.Nullable; +import com.google.gerrit.reviewdb.client.Account; +import com.google.gerrit.reviewdb.client.Change; +import com.google.gerrit.reviewdb.client.ChangeMessage; +import com.google.gerrit.reviewdb.client.Comment; +import com.google.gerrit.reviewdb.client.PatchLineComment; +import com.google.gerrit.reviewdb.client.PatchLineComment.Status; +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.server.ReviewDb; +import com.google.gerrit.reviewdb.server.ReviewDbUtil; +import com.google.gerrit.server.CommentsUtil; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.config.GerritServerId; +import com.google.gerrit.server.notedb.ChangeBundle; +import com.google.gerrit.server.notedb.ChangeBundleReader; +import com.google.gerrit.server.notedb.ChangeDraftUpdate; +import com.google.gerrit.server.notedb.ChangeNoteUtil; +import com.google.gerrit.server.notedb.ChangeNotes; +import com.google.gerrit.server.notedb.ChangeUpdate; +import com.google.gerrit.server.notedb.NoteDbChangeState; +import com.google.gerrit.server.notedb.NoteDbChangeState.PrimaryStorage; +import com.google.gerrit.server.notedb.NoteDbUpdateManager; +import com.google.gerrit.server.notedb.NoteDbUpdateManager.OpenRepo; +import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result; +import com.google.gerrit.server.notedb.NotesMigration; +import com.google.gerrit.server.notedb.ReviewerStateInternal; +import com.google.gerrit.server.patch.PatchListCache; +import com.google.gerrit.server.project.NoSuchChangeException; +import com.google.gerrit.server.project.ProjectCache; +import com.google.gerrit.server.update.ChainedReceiveCommands; +import com.google.gwtorm.client.Key; +import com.google.gwtorm.server.Access; +import com.google.gwtorm.server.AtomicUpdate; +import com.google.gwtorm.server.OrmException; +import com.google.gwtorm.server.SchemaFactory; +import com.google.inject.Inject; +import java.io.IOException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.TreeMap; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.transport.ReceiveCommand; + +public class ChangeRebuilderImpl extends ChangeRebuilder { + /** + * The maximum amount of time between the ReviewDb timestamp of the first and last events batched + * together into a single NoteDb update. + * + * <p>Used to account for the fact that different records with their own timestamps (e.g. {@link + * PatchSetApproval} and {@link ChangeMessage}) historically didn't necessarily use the same + * timestamp, and tended to call {@code System.currentTimeMillis()} independently. + */ + public static final long MAX_WINDOW_MS = SECONDS.toMillis(3); + + /** + * The maximum amount of time between two consecutive events to consider them to be in the same + * batch. + */ + static final long MAX_DELTA_MS = SECONDS.toMillis(1); + + private final ChangeBundleReader bundleReader; + private final ChangeDraftUpdate.Factory draftUpdateFactory; + private final ChangeNoteUtil changeNoteUtil; + private final ChangeNotes.Factory notesFactory; + private final ChangeUpdate.Factory updateFactory; + private final CommentsUtil commentsUtil; + private final NoteDbUpdateManager.Factory updateManagerFactory; + private final NotesMigration migration; + private final PatchListCache patchListCache; + private final PersonIdent serverIdent; + private final ProjectCache projectCache; + private final String serverId; + private final long skewMs; + + @Inject + ChangeRebuilderImpl( + @GerritServerConfig Config cfg, + SchemaFactory<ReviewDb> schemaFactory, + ChangeBundleReader bundleReader, + ChangeDraftUpdate.Factory draftUpdateFactory, + ChangeNoteUtil changeNoteUtil, + ChangeNotes.Factory notesFactory, + ChangeUpdate.Factory updateFactory, + CommentsUtil commentsUtil, + NoteDbUpdateManager.Factory updateManagerFactory, + NotesMigration migration, + PatchListCache patchListCache, + @GerritPersonIdent PersonIdent serverIdent, + @Nullable ProjectCache projectCache, + @GerritServerId String serverId) { + super(schemaFactory); + this.bundleReader = bundleReader; + this.draftUpdateFactory = draftUpdateFactory; + this.changeNoteUtil = changeNoteUtil; + this.notesFactory = notesFactory; + this.updateFactory = updateFactory; + this.commentsUtil = commentsUtil; + this.updateManagerFactory = updateManagerFactory; + this.migration = migration; + this.patchListCache = patchListCache; + this.serverIdent = serverIdent; + this.projectCache = projectCache; + this.serverId = serverId; + this.skewMs = NoteDbChangeState.getReadOnlySkew(cfg); + } + + @Override + public Result rebuild(ReviewDb db, Change.Id changeId) throws IOException, OrmException { + return rebuild(db, changeId, true); + } + + @Override + public Result rebuildEvenIfReadOnly(ReviewDb db, Change.Id changeId) + throws IOException, OrmException { + return rebuild(db, changeId, false); + } + + private Result rebuild(ReviewDb db, Change.Id changeId, boolean checkReadOnly) + throws IOException, OrmException { + db = ReviewDbUtil.unwrapDb(db); + // Read change just to get project; this instance is then discarded so we can read a consistent + // ChangeBundle inside a transaction. + Change change = db.changes().get(changeId); + if (change == null) { + throw new NoSuchChangeException(changeId); + } + try (NoteDbUpdateManager manager = updateManagerFactory.create(change.getProject())) { + buildUpdates(manager, bundleReader.fromReviewDb(db, changeId)); + return execute(db, changeId, manager, checkReadOnly, true); + } + } + + @Override + public Result rebuild(NoteDbUpdateManager manager, ChangeBundle bundle) + throws NoSuchChangeException, IOException, OrmException { + Change change = new Change(bundle.getChange()); + buildUpdates(manager, bundle); + return manager.stageAndApplyDelta(change); + } + + @Override + public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId) + throws IOException, OrmException { + db = ReviewDbUtil.unwrapDb(db); + Change change = checkNoteDbState(ChangeNotes.readOneReviewDbChange(db, changeId)); + if (change == null) { + throw new NoSuchChangeException(changeId); + } + NoteDbUpdateManager manager = updateManagerFactory.create(change.getProject()); + buildUpdates(manager, bundleReader.fromReviewDb(db, changeId)); + manager.stage(); + return manager; + } + + @Override + public Result execute(ReviewDb db, Change.Id changeId, NoteDbUpdateManager manager) + throws OrmException, IOException { + return execute(db, changeId, manager, true, true); + } + + public Result execute( + ReviewDb db, + Change.Id changeId, + NoteDbUpdateManager manager, + boolean checkReadOnly, + boolean executeManager) + throws OrmException, IOException { + db = ReviewDbUtil.unwrapDb(db); + Change change = checkNoteDbState(ChangeNotes.readOneReviewDbChange(db, changeId)); + if (change == null) { + throw new NoSuchChangeException(changeId); + } + + String oldNoteDbStateStr = change.getNoteDbState(); + Result r = manager.stageAndApplyDelta(change); + String newNoteDbStateStr = change.getNoteDbState(); + if (newNoteDbStateStr == null) { + throw new OrmException( + String.format( + "Rebuilding change %s produced no writes to NoteDb: %s", + changeId, bundleReader.fromReviewDb(db, changeId))); + } + NoteDbChangeState newNoteDbState = + requireNonNull(NoteDbChangeState.parse(changeId, newNoteDbStateStr)); + try { + db.changes() + .atomicUpdate( + changeId, + new AtomicUpdate<Change>() { + @Override + public Change update(Change change) { + if (checkReadOnly) { + NoteDbChangeState.checkNotReadOnly(change, skewMs); + } + String currNoteDbStateStr = change.getNoteDbState(); + if (Objects.equals(currNoteDbStateStr, newNoteDbStateStr)) { + // Another thread completed the same rebuild we were about to. + throw new AbortUpdateException(); + } else if (!Objects.equals(oldNoteDbStateStr, currNoteDbStateStr)) { + // Another thread updated the state to something else. + throw new ConflictingUpdateRuntimeException(change, oldNoteDbStateStr); + } + change.setNoteDbState(newNoteDbStateStr); + return change; + } + }); + } catch (ConflictingUpdateRuntimeException e) { + // Rethrow as an OrmException so the caller knows to use staged results. Strictly speaking + // they are not completely up to date, but result we send to the caller is the same as if this + // rebuild had executed before the other thread. + throw new ConflictingUpdateException(e); + } catch (AbortUpdateException e) { + if (newNoteDbState.isUpToDate( + manager.getChangeRepo().cmds.getRepoRefCache(), + manager.getAllUsersRepo().cmds.getRepoRefCache())) { + // If the state in ReviewDb matches NoteDb at this point, it means another thread + // successfully completed this rebuild. It's ok to not execute the update in this case, + // since the object referenced in the Result was flushed to the repo by whatever thread won + // the race. + return r; + } + // If the state doesn't match, that means another thread attempted this rebuild, but + // failed. Fall through and try to update the ref again. + } + if (migration.failChangeWrites()) { + // Don't even attempt to execute if read-only, it would fail anyway. But do throw an exception + // to the caller so they know to use the staged results instead of reading from the repo. + throw new OrmException(NoteDbUpdateManager.CHANGES_READ_ONLY); + } + if (executeManager) { + manager.execute(); + } + return r; + } + + static Change checkNoteDbState(Change c) throws OrmException { + // Can only rebuild a change if its primary storage is ReviewDb. + NoteDbChangeState s = NoteDbChangeState.parse(c); + if (s != null && s.getPrimaryStorage() != PrimaryStorage.REVIEW_DB) { + throw new OrmException(String.format("cannot rebuild change %s with state %s", c.getId(), s)); + } + return c; + } + + @Override + public void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle) + throws IOException, OrmException { + manager.setCheckExpectedState(false).setRefLogMessage("Rebuilding change"); + Change change = new Change(bundle.getChange()); + if (bundle.getPatchSets().isEmpty()) { + throw new NoPatchSetsException(change.getId()); + } + if (change.getLastUpdatedOn().compareTo(change.getCreatedOn()) < 0) { + // A bug in data migration might set created_on to the time of the migration. The + // correct timestamps were lost, but we can at least set it so created_on is not after + // last_updated_on. + // See https://bugs.chromium.org/p/gerrit/issues/detail?id=7397 + change.setCreatedOn(change.getLastUpdatedOn()); + } + + // We will rebuild all events, except for draft comments, in buckets based on author and + // timestamp. + List<Event> events = new ArrayList<>(); + ListMultimap<Account.Id, DraftCommentEvent> draftCommentEvents = + MultimapBuilder.hashKeys().arrayListValues().build(); + + events.addAll(getHashtagsEvents(change, manager)); + + // Delete ref only after hashtags have been read. + deleteChangeMetaRef(change, manager.getChangeRepo().cmds); + deleteDraftRefs(change, manager.getAllUsersRepo()); + + Integer minPsNum = getMinPatchSetNum(bundle); + TreeMap<PatchSet.Id, PatchSetEvent> patchSetEvents = + new TreeMap<>(ReviewDbUtil.intKeyOrdering()); + + for (PatchSet ps : bundle.getPatchSets()) { + PatchSetEvent pse = new PatchSetEvent(change, ps, manager.getChangeRepo().rw); + patchSetEvents.put(ps.getId(), pse); + events.add(pse); + for (Comment c : getComments(bundle, serverId, Status.PUBLISHED, ps)) { + CommentEvent e = new CommentEvent(c, change, ps, patchListCache); + events.add(e.addDep(pse)); + } + for (Comment c : getComments(bundle, serverId, Status.DRAFT, ps)) { + DraftCommentEvent e = new DraftCommentEvent(c, change, ps, patchListCache); + draftCommentEvents.put(c.author.getId(), e); + } + } + ensurePatchSetOrder(patchSetEvents); + + for (PatchSetApproval psa : bundle.getPatchSetApprovals()) { + PatchSetEvent pse = patchSetEvents.get(psa.getPatchSetId()); + if (pse != null) { + events.add(new ApprovalEvent(psa, change.getCreatedOn()).addDep(pse)); + } + } + + for (Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> r : + bundle.getReviewers().asTable().cellSet()) { + events.add(new ReviewerEvent(r, change.getCreatedOn())); + } + + Change noteDbChange = new Change(null, null, null, null, null); + for (ChangeMessage msg : bundle.getChangeMessages()) { + Event msgEvent = new ChangeMessageEvent(change, noteDbChange, msg, change.getCreatedOn()); + if (msg.getPatchSetId() != null) { + PatchSetEvent pse = patchSetEvents.get(msg.getPatchSetId()); + if (pse == null) { + continue; // Ignore events for missing patch sets. + } + msgEvent.addDep(pse); + } + events.add(msgEvent); + } + + sortAndFillEvents(change, noteDbChange, bundle.getPatchSets(), events, minPsNum); + + EventList<Event> el = new EventList<>(); + for (Event e : events) { + if (!el.canAdd(e)) { + flushEventsToUpdate(manager, el, change); + checkState(el.canAdd(e)); + } + el.add(e); + } + flushEventsToUpdate(manager, el, change); + + EventList<DraftCommentEvent> plcel = new EventList<>(); + for (Account.Id author : draftCommentEvents.keys()) { + for (DraftCommentEvent e : Ordering.natural().sortedCopy(draftCommentEvents.get(author))) { + if (!plcel.canAdd(e)) { + flushEventsToDraftUpdate(manager, plcel, change); + checkState(plcel.canAdd(e)); + } + plcel.add(e); + } + flushEventsToDraftUpdate(manager, plcel, change); + } + } + + private static Integer getMinPatchSetNum(ChangeBundle bundle) { + Integer minPsNum = null; + for (PatchSet ps : bundle.getPatchSets()) { + int n = ps.getId().get(); + if (minPsNum == null || n < minPsNum) { + minPsNum = n; + } + } + return minPsNum; + } + + private static void ensurePatchSetOrder(TreeMap<PatchSet.Id, PatchSetEvent> events) { + if (events.isEmpty()) { + return; + } + Iterator<PatchSetEvent> it = events.values().iterator(); + PatchSetEvent curr = it.next(); + while (it.hasNext()) { + PatchSetEvent next = it.next(); + next.addDep(curr); + curr = next; + } + } + + private static List<Comment> getComments( + ChangeBundle bundle, String serverId, PatchLineComment.Status status, PatchSet ps) { + return bundle.getPatchLineComments().stream() + .filter(c -> c.getPatchSetId().equals(ps.getId()) && c.getStatus() == status) + .map(plc -> plc.asComment(serverId)) + .sorted(CommentsUtil.COMMENT_ORDER) + .collect(toList()); + } + + private void sortAndFillEvents( + Change change, + Change noteDbChange, + ImmutableCollection<PatchSet> patchSets, + List<Event> events, + Integer minPsNum) { + Event finalUpdates = new FinalUpdatesEvent(change, noteDbChange, patchSets); + events.add(finalUpdates); + setPostSubmitDeps(events); + new EventSorter(events).sort(); + + // Ensure the first event in the list creates the change, setting the author and any required + // footers. Also force the creation time of the first patch set to match the creation time of + // the change. + Event first = events.get(0); + if (first instanceof PatchSetEvent && change.getOwner().equals(first.user)) { + first.when = change.getCreatedOn(); + ((PatchSetEvent) first).createChange = true; + } else { + events.add(0, new CreateChangeEvent(change, minPsNum)); + } + + // Final pass to correct some inconsistencies. + // + // First, fill in any missing patch set IDs using the latest patch set of the change at the time + // of the event, because NoteDb can't represent actions with no associated patch set ID. This + // workaround is as if a user added a ChangeMessage on the change by replying from the latest + // patch set. + // + // Start with the first patch set that actually exists. If there are no patch sets at all, + // minPsNum will be null, so just bail and use 1 as the patch set ID. + // + // Second, ensure timestamps are nondecreasing, by copying the previous timestamp if this + // happens. This assumes that the only way this can happen is due to dependency constraints, and + // it is ok to give an event the same timestamp as one of its dependencies. + int ps = firstNonNull(minPsNum, 1); + for (int i = 0; i < events.size(); i++) { + Event e = events.get(i); + if (e.psId == null) { + e.psId = new PatchSet.Id(change.getId(), ps); + } else { + ps = Math.max(ps, e.psId.get()); + } + + if (i > 0) { + Event p = events.get(i - 1); + if (e.when.before(p.when)) { + e.when = p.when; + } + } + } + } + + private void setPostSubmitDeps(List<Event> events) { + Optional<Event> submitEvent = + Lists.reverse(events).stream().filter(Event::isSubmit).findFirst(); + if (submitEvent.isPresent()) { + events.stream().filter(Event::isPostSubmitApproval).forEach(e -> e.addDep(submitEvent.get())); + } + } + + private void flushEventsToUpdate( + NoteDbUpdateManager manager, EventList<Event> events, Change change) + throws OrmException, IOException { + if (events.isEmpty()) { + return; + } + Comparator<String> labelNameComparator; + if (projectCache != null) { + labelNameComparator = projectCache.get(change.getProject()).getLabelTypes().nameComparator(); + } else { + // No project cache available, bail and use natural ordering; there's no semantic difference + // anyway difference. + labelNameComparator = Ordering.natural(); + } + ChangeUpdate update = + updateFactory.create( + change, + events.getAccountId(), + events.getRealAccountId(), + newAuthorIdent(events), + events.getWhen(), + labelNameComparator); + update.setAllowWriteToNewRef(true); + update.setPatchSetId(events.getPatchSetId()); + update.setTag(events.getTag()); + for (Event e : events) { + e.apply(update); + } + manager.add(update); + events.clear(); + } + + private void flushEventsToDraftUpdate( + NoteDbUpdateManager manager, EventList<DraftCommentEvent> events, Change change) { + if (events.isEmpty()) { + return; + } + ChangeDraftUpdate update = + draftUpdateFactory.create( + change, + events.getAccountId(), + events.getRealAccountId(), + newAuthorIdent(events), + events.getWhen()); + update.setPatchSetId(events.getPatchSetId()); + for (DraftCommentEvent e : events) { + e.applyDraft(update); + } + manager.add(update); + events.clear(); + } + + private PersonIdent newAuthorIdent(EventList<?> events) { + Account.Id id = events.getAccountId(); + if (id == null) { + return new PersonIdent(serverIdent, events.getWhen()); + } + return changeNoteUtil.newIdent(id, events.getWhen(), serverIdent); + } + + private List<HashtagsEvent> getHashtagsEvents(Change change, NoteDbUpdateManager manager) + throws IOException { + String refName = changeMetaRef(change.getId()); + Optional<ObjectId> old = manager.getChangeRepo().getObjectId(refName); + if (!old.isPresent()) { + return Collections.emptyList(); + } + + RevWalk rw = manager.getChangeRepo().rw; + List<HashtagsEvent> events = new ArrayList<>(); + rw.reset(); + rw.markStart(rw.parseCommit(old.get())); + for (RevCommit commit : rw) { + Account.Id authorId; + try { + authorId = + changeNoteUtil + .getLegacyChangeNoteRead() + .parseIdent(commit.getAuthorIdent(), change.getId()); + } catch (ConfigInvalidException e) { + continue; // Corrupt data, no valid hashtags in this commit. + } + PatchSet.Id psId = parsePatchSetId(change, commit); + Set<String> hashtags = parseHashtags(commit); + if (authorId == null || psId == null || hashtags == null) { + continue; + } + + Timestamp commitTime = new Timestamp(commit.getCommitterIdent().getWhen().getTime()); + events.add(new HashtagsEvent(psId, authorId, commitTime, hashtags, change.getCreatedOn())); + } + return events; + } + + private Set<String> parseHashtags(RevCommit commit) { + List<String> hashtagsLines = commit.getFooterLines(FOOTER_HASHTAGS); + if (hashtagsLines.isEmpty() || hashtagsLines.size() > 1) { + return null; + } + + if (hashtagsLines.get(0).isEmpty()) { + return ImmutableSet.of(); + } + return Sets.newHashSet(Splitter.on(',').split(hashtagsLines.get(0))); + } + + private PatchSet.Id parsePatchSetId(Change change, RevCommit commit) { + List<String> psIdLines = commit.getFooterLines(FOOTER_PATCH_SET); + if (psIdLines.size() != 1) { + return null; + } + Integer psId = Ints.tryParse(psIdLines.get(0)); + if (psId == null) { + return null; + } + return new PatchSet.Id(change.getId(), psId); + } + + private void deleteChangeMetaRef(Change change, ChainedReceiveCommands cmds) throws IOException { + String refName = changeMetaRef(change.getId()); + Optional<ObjectId> old = cmds.get(refName); + if (old.isPresent()) { + cmds.add(new ReceiveCommand(old.get(), ObjectId.zeroId(), refName)); + } + } + + private void deleteDraftRefs(Change change, OpenRepo allUsersRepo) throws IOException { + for (Ref r : + allUsersRepo + .repo + .getRefDatabase() + .getRefsByPrefix(RefNames.refsDraftCommentsPrefix(change.getId()))) { + allUsersRepo.cmds.add(new ReceiveCommand(r.getObjectId(), ObjectId.zeroId(), r.getName())); + } + } + + static void createChange(ChangeUpdate update, Change change) { + update.setSubjectForCommit("Create change"); + update.setChangeId(change.getKey().get()); + update.setBranch(change.getDest().get()); + update.setSubject(change.getOriginalSubject()); + if (change.getRevertOf() != null) { + update.setRevertOf(change.getRevertOf().get()); + } + } + + @Override + public void rebuildReviewDb(ReviewDb db, Project.NameKey project, Change.Id changeId) + throws OrmException { + // TODO(dborowitz): Fail fast if changes tables are disabled in ReviewDb. + ChangeNotes notes = notesFactory.create(db, project, changeId); + ChangeBundle bundle = ChangeBundle.fromNotes(commentsUtil, notes); + + db = ReviewDbUtil.unwrapDb(db); + db.changes().beginTransaction(changeId); + try { + Change c = db.changes().get(changeId); + if (c != null) { + PrimaryStorage ps = PrimaryStorage.of(c); + switch (ps) { + case REVIEW_DB: + return; // Nothing to do. + case NOTE_DB: + break; // Continue and rebuild. + default: + throw new OrmException("primary storage of " + changeId + " is " + ps); + } + } else { + c = notes.getChange(); + } + db.changes().upsert(Collections.singleton(c)); + putExactlyEntities( + db.changeMessages(), db.changeMessages().byChange(c.getId()), bundle.getChangeMessages()); + putExactlyEntities(db.patchSets(), db.patchSets().byChange(c.getId()), bundle.getPatchSets()); + putExactlyEntities( + db.patchSetApprovals(), + db.patchSetApprovals().byChange(c.getId()), + bundle.getPatchSetApprovals()); + putExactlyEntities( + db.patchComments(), + db.patchComments().byChange(c.getId()), + bundle.getPatchLineComments()); + db.commit(); + } finally { + db.rollback(); + } + } + + private static <T, K extends Key<?>> void putExactlyEntities( + Access<T, K> access, Iterable<T> existing, Collection<T> ents) throws OrmException { + Set<K> toKeep = access.toMap(ents).keySet(); + access.delete( + FluentIterable.from(existing).filter(e -> !toKeep.contains(access.primaryKey(e)))); + access.upsert(ents); + } +} |