blob: 8d5af698bce3c9e4a723dafdeeda9731667fd445 [file] [log] [blame]
// 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.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.ChangeNoteUtil.FOOTER_HASHTAGS;
import static com.google.gerrit.server.notedb.ChangeNoteUtil.FOOTER_PATCH_SET;
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.account.AccountCache;
import com.google.gerrit.server.config.AnonymousCowardName;
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 AccountCache accountCache;
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 anonymousCowardName;
private final String serverId;
private final long skewMs;
@Inject
ChangeRebuilderImpl(
@GerritServerConfig Config cfg,
SchemaFactory<ReviewDb> schemaFactory,
AccountCache accountCache,
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,
@AnonymousCowardName String anonymousCowardName,
@GerritServerId String serverId) {
super(schemaFactory);
this.accountCache = accountCache;
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.anonymousCowardName = anonymousCowardName;
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 =
checkNotNull(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(
accountCache.get(id).getAccount(), events.getWhen(), serverIdent, anonymousCowardName);
}
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.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()
.getRefs(RefNames.refsDraftCommentsPrefix(change.getId()))
.values()) {
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);
}
}