summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java')
-rw-r--r--java/com/google/gerrit/server/notedb/rebuild/ChangeRebuilderImpl.java685
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);
+ }
+}