blob: 08acbaddaf295db85956338d7c633ad491abd788 [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;
import static com.google.common.base.MoreObjects.firstNonNull;
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.PatchLineCommentsUtil.setCommentRevId;
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 com.google.common.base.MoreObjects;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
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.FormatUtil;
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.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.GerritPersonIdent;
import com.google.gerrit.server.PatchLineCommentsUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.git.ChainedReceiveCommands;
import com.google.gerrit.server.notedb.NoteDbUpdateManager.OpenRepo;
import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gwtorm.server.AtomicUpdate;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.OrmRuntimeException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TextProgressMonitor;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ChangeRebuilderImpl extends ChangeRebuilder {
private static final Logger log =
LoggerFactory.getLogger(ChangeRebuilderImpl.class);
/**
* 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.
*/
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.
*/
private static final long MAX_DELTA_MS = SECONDS.toMillis(1);
private final AccountCache accountCache;
private final ChangeDraftUpdate.Factory draftUpdateFactory;
private final ChangeNoteUtil changeNoteUtil;
private final ChangeUpdate.Factory updateFactory;
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;
@Inject
ChangeRebuilderImpl(SchemaFactory<ReviewDb> schemaFactory,
AccountCache accountCache,
ChangeDraftUpdate.Factory draftUpdateFactory,
ChangeNoteUtil changeNoteUtil,
ChangeUpdate.Factory updateFactory,
NoteDbUpdateManager.Factory updateManagerFactory,
NotesMigration migration,
PatchListCache patchListCache,
@GerritPersonIdent PersonIdent serverIdent,
@Nullable ProjectCache projectCache,
@AnonymousCowardName String anonymousCowardName) {
super(schemaFactory);
this.accountCache = accountCache;
this.draftUpdateFactory = draftUpdateFactory;
this.changeNoteUtil = changeNoteUtil;
this.updateFactory = updateFactory;
this.updateManagerFactory = updateManagerFactory;
this.migration = migration;
this.patchListCache = patchListCache;
this.serverIdent = serverIdent;
this.projectCache = projectCache;
this.anonymousCowardName = anonymousCowardName;
}
@Override
public Result rebuild(ReviewDb db, Change.Id changeId)
throws NoSuchChangeException, IOException, OrmException,
ConfigInvalidException {
db = ReviewDbUtil.unwrapDb(db);
Change change = db.changes().get(changeId);
if (change == null) {
throw new NoSuchChangeException(changeId);
}
try (NoteDbUpdateManager manager =
updateManagerFactory.create(change.getProject())) {
buildUpdates(manager, ChangeBundle.fromReviewDb(db, changeId));
return execute(db, changeId, manager);
}
}
private static class AbortUpdateException extends OrmRuntimeException {
private static final long serialVersionUID = 1L;
AbortUpdateException() {
super("aborted");
}
}
private static class ConflictingUpdateException extends OrmRuntimeException {
private static final long serialVersionUID = 1L;
ConflictingUpdateException(Change change, String expectedNoteDbState) {
super(String.format(
"Expected change %s to have noteDbState %s but was %s",
change.getId(), expectedNoteDbState, change.getNoteDbState()));
}
}
@Override
public Result rebuild(NoteDbUpdateManager manager,
ChangeBundle bundle) throws NoSuchChangeException, IOException,
OrmException, ConfigInvalidException {
Change change = new Change(bundle.getChange());
buildUpdates(manager, bundle);
return manager.stageAndApplyDelta(change);
}
@Override
public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId)
throws NoSuchChangeException, IOException, OrmException {
db = ReviewDbUtil.unwrapDb(db);
Change change = db.changes().get(changeId);
if (change == null) {
throw new NoSuchChangeException(changeId);
}
NoteDbUpdateManager manager =
updateManagerFactory.create(change.getProject());
buildUpdates(manager, ChangeBundle.fromReviewDb(db, changeId));
manager.stage();
return manager;
}
@Override
public Result execute(ReviewDb db, Change.Id changeId,
NoteDbUpdateManager manager) throws NoSuchChangeException, OrmException,
IOException {
db = ReviewDbUtil.unwrapDb(db);
Change change = db.changes().get(changeId);
if (change == null) {
throw new NoSuchChangeException(changeId);
}
final String oldNoteDbState = change.getNoteDbState();
Result r = manager.stageAndApplyDelta(change);
final String newNoteDbState = change.getNoteDbState();
try {
db.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() {
@Override
public Change update(Change change) {
String currNoteDbState = change.getNoteDbState();
if (Objects.equals(currNoteDbState, newNoteDbState)) {
// Another thread completed the same rebuild we were about to.
throw new AbortUpdateException();
} else if (!Objects.equals(oldNoteDbState, currNoteDbState)) {
// Another thread updated the state to something else.
throw new ConflictingUpdateException(change, oldNoteDbState);
}
change.setNoteDbState(newNoteDbState);
return change;
}
});
} catch (ConflictingUpdateException 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 OrmException(e.getMessage());
} catch (AbortUpdateException e) {
if (NoteDbChangeState.parse(changeId, 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);
}
manager.execute();
return r;
}
@Override
public boolean rebuildProject(ReviewDb db,
ImmutableMultimap<Project.NameKey, Change.Id> allChanges,
Project.NameKey project, Repository allUsersRepo)
throws NoSuchChangeException, IOException, OrmException,
ConfigInvalidException {
checkArgument(allChanges.containsKey(project));
boolean ok = true;
ProgressMonitor pm = new TextProgressMonitor(new PrintWriter(System.out));
pm.beginTask(
FormatUtil.elide(project.get(), 50), allChanges.get(project).size());
try (NoteDbUpdateManager manager = updateManagerFactory.create(project);
ObjectInserter allUsersInserter = allUsersRepo.newObjectInserter();
RevWalk allUsersRw = new RevWalk(allUsersInserter.newReader())) {
manager.setAllUsersRepo(allUsersRepo, allUsersRw, allUsersInserter,
new ChainedReceiveCommands(allUsersRepo));
for (Change.Id changeId : allChanges.get(project)) {
try {
buildUpdates(manager, ChangeBundle.fromReviewDb(db, changeId));
} catch (NoPatchSetsException e) {
log.warn(e.getMessage());
} catch (Throwable t) {
log.error("Failed to rebuild change " + changeId, t);
ok = false;
}
pm.update(1);
}
manager.execute();
} finally {
pm.endTask();
}
return ok;
}
private void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle)
throws IOException, OrmException {
manager.setCheckExpectedState(false);
Change change = new Change(bundle.getChange());
if (bundle.getPatchSets().isEmpty()) {
throw new NoPatchSetsException(change.getId());
}
PatchSet.Id currPsId = change.currentPatchSetId();
// We will rebuild all events, except for draft comments, in buckets based
// on author and timestamp.
List<Event> events = new ArrayList<>();
Multimap<Account.Id, PatchLineCommentEvent> draftCommentEvents =
ArrayListMultimap.create();
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);
Set<PatchSet.Id> psIds =
Sets.newHashSetWithExpectedSize(bundle.getPatchSets().size());
for (PatchSet ps : bundle.getPatchSets()) {
if (ps.getId().get() > currPsId.get()) {
log.info(
"Skipping patch set {}, which is higher than current patch set {}",
ps.getId(), currPsId);
continue;
}
psIds.add(ps.getId());
events.add(new PatchSetEvent(
change, ps, manager.getChangeRepo().rw));
for (PatchLineComment c : getPatchLineComments(bundle, ps)) {
PatchLineCommentEvent e =
new PatchLineCommentEvent(c, change, ps, patchListCache);
if (c.getStatus() == Status.PUBLISHED) {
events.add(e);
} else {
draftCommentEvents.put(c.getAuthor(), e);
}
}
}
for (PatchSetApproval psa : bundle.getPatchSetApprovals()) {
if (psIds.contains(psa.getPatchSetId())) {
events.add(new ApprovalEvent(psa, change.getCreatedOn()));
}
}
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()) {
if (msg.getPatchSetId() == null || psIds.contains(msg.getPatchSetId())) {
events.add(
new ChangeMessageEvent(msg, noteDbChange, change.getCreatedOn()));
}
}
sortAndFillEvents(change, noteDbChange, 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<PatchLineCommentEvent> plcel = new EventList<>();
for (Account.Id author : draftCommentEvents.keys()) {
for (PatchLineCommentEvent e :
EVENT_ORDER.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 List<PatchLineComment> getPatchLineComments(ChangeBundle bundle,
final PatchSet ps) {
return FluentIterable.from(bundle.getPatchLineComments())
.filter(new Predicate<PatchLineComment>() {
@Override
public boolean apply(PatchLineComment in) {
return in.getPatchSetId().equals(ps.getId());
}
}).toSortedList(PatchLineCommentsUtil.PLC_ORDER);
}
private void sortAndFillEvents(Change change, Change noteDbChange,
List<Event> events, Integer minPsNum) {
Collections.sort(events, EVENT_ORDER);
events.add(new FinalUpdatesEvent(change, noteDbChange));
// Ensure the first event in the list creates the change, setting the author
// and any required footers.
Event first = events.get(0);
if (first instanceof PatchSetEvent && change.getOwner().equals(first.who)) {
((PatchSetEvent) first).createChange = true;
} else {
events.add(0, new CreateChangeEvent(change, minPsNum));
}
// 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. The corresponding patch set won't exist, but this change is
// probably corrupt anyway, as deleting the last draft patch set should have
// deleted the whole change.
int ps = firstNonNull(minPsNum, 1);
for (Event e : events) {
if (e.psId == null) {
e.psId = new PatchSet.Id(change.getId(), ps);
} else {
ps = Math.max(ps, e.psId.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.newAuthorIdent(),
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<PatchLineCommentEvent> events, Change change)
throws OrmException {
if (events.isEmpty()) {
return;
}
ChangeDraftUpdate update = draftUpdateFactory.create(
change,
events.getAccountId(),
events.newAuthorIdent(),
events.getWhen());
update.setPatchSetId(events.getPatchSetId());
for (PatchLineCommentEvent e : events) {
e.applyDraft(update);
}
manager.add(update);
events.clear();
}
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()));
}
}
private static final Ordering<Event> EVENT_ORDER = new Ordering<Event>() {
@Override
public int compare(Event a, Event b) {
return ComparisonChain.start()
.compare(a.when, b.when)
.compareTrueFirst(isPatchSet(a), isPatchSet(b))
.compareTrueFirst(a.predatesChange, b.predatesChange)
.compare(a.who, b.who, ReviewDbUtil.intKeyOrdering())
.compare(a.psId, b.psId, ReviewDbUtil.intKeyOrdering().nullsLast())
.result();
}
private boolean isPatchSet(Event e) {
return e instanceof PatchSetEvent;
}
};
private abstract static class Event {
// NOTE: EventList only supports direct subclasses, not an arbitrary
// hierarchy.
final Account.Id who;
final Timestamp when;
final String tag;
final boolean predatesChange;
PatchSet.Id psId;
protected Event(PatchSet.Id psId, Account.Id who, Timestamp when,
Timestamp changeCreatedOn, String tag) {
this.psId = psId;
this.who = who;
this.tag = tag;
// Truncate timestamps at the change's createdOn timestamp.
predatesChange = when.before(changeCreatedOn);
this.when = predatesChange ? changeCreatedOn : when;
}
protected void checkUpdate(AbstractChangeUpdate update) {
checkState(Objects.equals(update.getPatchSetId(), psId),
"cannot apply event for %s to update for %s",
update.getPatchSetId(), psId);
checkState(when.getTime() - update.getWhen().getTime() <= MAX_WINDOW_MS,
"event at %s outside update window starting at %s",
when, update.getWhen());
checkState(Objects.equals(update.getNullableAccountId(), who),
"cannot apply event by %s to update by %s",
who, update.getNullableAccountId());
}
/**
* @return whether this event type must be unique per {@link ChangeUpdate},
* i.e. there may be at most one of this type.
*/
abstract boolean uniquePerUpdate();
abstract void apply(ChangeUpdate update) throws OrmException, IOException;
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("psId", psId)
.add("who", who)
.add("when", when)
.toString();
}
}
private class EventList<E extends Event> extends ArrayList<E> {
private static final long serialVersionUID = 1L;
private E getLast() {
return get(size() - 1);
}
private long getLastTime() {
return getLast().when.getTime();
}
private long getFirstTime() {
return get(0).when.getTime();
}
boolean canAdd(E e) {
if (isEmpty()) {
return true;
}
if (e instanceof FinalUpdatesEvent) {
return false; // FinalUpdatesEvent always gets its own update.
}
Event last = getLast();
if (!Objects.equals(e.who, last.who)
|| !e.psId.equals(last.psId)
|| !Objects.equals(e.tag, last.tag)) {
return false; // Different patch set, author, or tag.
}
long t = e.when.getTime();
long tFirst = getFirstTime();
long tLast = getLastTime();
checkArgument(t >= tLast,
"event %s is before previous event in list %s", e, last);
if (t - tLast > MAX_DELTA_MS || t - tFirst > MAX_WINDOW_MS) {
return false; // Too much time elapsed.
}
if (!e.uniquePerUpdate()) {
return true;
}
for (Event o : this) {
if (e.getClass() == o.getClass()) {
return false; // Only one event of this type allowed per update.
}
}
// TODO(dborowitz): Additional heuristics, like keeping events separate if
// they affect overlapping fields within a single entity.
return true;
}
Timestamp getWhen() {
return get(0).when;
}
PatchSet.Id getPatchSetId() {
PatchSet.Id id = checkNotNull(get(0).psId);
for (int i = 1; i < size(); i++) {
checkState(get(i).psId.equals(id),
"mismatched patch sets in EventList: %s != %s", id, get(i).psId);
}
return id;
}
Account.Id getAccountId() {
Account.Id id = get(0).who;
for (int i = 1; i < size(); i++) {
checkState(Objects.equals(id, get(i).who),
"mismatched users in EventList: %s != %s", id, get(i).who);
}
return id;
}
PersonIdent newAuthorIdent() {
Account.Id id = getAccountId();
if (id == null) {
return new PersonIdent(serverIdent, getWhen());
}
return changeNoteUtil.newIdent(
accountCache.get(id).getAccount(), getWhen(), serverIdent,
anonymousCowardName);
}
String getTag() {
return getLast().tag;
}
}
private 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());
}
private static class CreateChangeEvent extends Event {
private final Change change;
private static PatchSet.Id psId(Change change, Integer minPsNum) {
int n;
if (minPsNum == null) {
// There were no patch sets for the change at all, so something is very
// wrong. Bail and use 1 as the patch set.
n = 1;
} else {
n = minPsNum;
}
return new PatchSet.Id(change.getId(), n);
}
CreateChangeEvent(Change change, Integer minPsNum) {
super(psId(change, minPsNum), change.getOwner(), change.getCreatedOn(),
change.getCreatedOn(), null);
this.change = change;
}
@Override
boolean uniquePerUpdate() {
return true;
}
@Override
void apply(ChangeUpdate update) throws IOException, OrmException {
checkUpdate(update);
createChange(update, change);
}
}
private static class ApprovalEvent extends Event {
private PatchSetApproval psa;
ApprovalEvent(PatchSetApproval psa, Timestamp changeCreatedOn) {
super(psa.getPatchSetId(), psa.getAccountId(), psa.getGranted(),
changeCreatedOn, psa.getTag());
this.psa = psa;
}
@Override
boolean uniquePerUpdate() {
return false;
}
@Override
void apply(ChangeUpdate update) {
checkUpdate(update);
update.putApproval(psa.getLabel(), psa.getValue());
}
}
private static class ReviewerEvent extends Event {
private Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> reviewer;
ReviewerEvent(
Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> reviewer,
Timestamp changeCreatedOn) {
super(
// Reviewers aren't generally associated with a particular patch set
// (although as an implementation detail they were in ReviewDb). Just
// use the latest patch set at the time of the event.
null,
reviewer.getColumnKey(), reviewer.getValue(), changeCreatedOn, null);
this.reviewer = reviewer;
}
@Override
boolean uniquePerUpdate() {
return false;
}
@Override
void apply(ChangeUpdate update) throws IOException, OrmException {
checkUpdate(update);
update.putReviewer(reviewer.getColumnKey(), reviewer.getRowKey());
}
}
private static class PatchSetEvent extends Event {
private final Change change;
private final PatchSet ps;
private final RevWalk rw;
private boolean createChange;
PatchSetEvent(Change change, PatchSet ps, RevWalk rw) {
super(ps.getId(), ps.getUploader(), ps.getCreatedOn(),
change.getCreatedOn(), null);
this.change = change;
this.ps = ps;
this.rw = rw;
}
@Override
boolean uniquePerUpdate() {
return true;
}
@Override
void apply(ChangeUpdate update) throws IOException, OrmException {
checkUpdate(update);
if (createChange) {
createChange(update, change);
} else {
update.setSubject(change.getSubject());
update.setSubjectForCommit("Create patch set " + ps.getPatchSetId());
}
setRevision(update, ps);
List<String> groups = ps.getGroups();
if (!groups.isEmpty()) {
update.setGroups(ps.getGroups());
}
if (ps.isDraft()) {
update.setPatchSetState(PatchSetState.DRAFT);
}
}
private void setRevision(ChangeUpdate update, PatchSet ps)
throws IOException {
String rev = ps.getRevision().get();
String cert = ps.getPushCertificate();
ObjectId id;
try {
id = ObjectId.fromString(rev);
} catch (InvalidObjectIdException e) {
update.setRevisionForMissingCommit(rev, cert);
return;
}
try {
update.setCommit(rw, id, cert);
} catch (MissingObjectException e) {
update.setRevisionForMissingCommit(rev, cert);
return;
}
}
}
private static class PatchLineCommentEvent extends Event {
public final PatchLineComment c;
private final Change change;
private final PatchSet ps;
private final PatchListCache cache;
PatchLineCommentEvent(PatchLineComment c, Change change, PatchSet ps,
PatchListCache cache) {
super(PatchLineCommentsUtil.getCommentPsId(c), c.getAuthor(),
c.getWrittenOn(), change.getCreatedOn(), c.getTag());
this.c = c;
this.change = change;
this.ps = ps;
this.cache = cache;
}
@Override
boolean uniquePerUpdate() {
return false;
}
@Override
void apply(ChangeUpdate update) throws OrmException {
checkUpdate(update);
if (c.getRevId() == null) {
setCommentRevId(c, cache, change, ps);
}
update.putComment(c);
}
void applyDraft(ChangeDraftUpdate draftUpdate) throws OrmException {
if (c.getRevId() == null) {
setCommentRevId(c, cache, change, ps);
}
draftUpdate.putComment(c);
}
}
private static class HashtagsEvent extends Event {
private final Set<String> hashtags;
HashtagsEvent(PatchSet.Id psId, Account.Id who, Timestamp when,
Set<String> hashtags, Timestamp changeCreatdOn) {
super(psId, who, when, changeCreatdOn,
// Somewhat confusingly, hashtags do not use the setTag method on
// AbstractChangeUpdate, so pass null as the tag.
null);
this.hashtags = hashtags;
}
@Override
boolean uniquePerUpdate() {
// Since these are produced from existing commits in the old NoteDb graph,
// we know that there must be one per commit in the rebuilt graph.
return true;
}
@Override
void apply(ChangeUpdate update) throws OrmException {
update.setHashtags(hashtags);
}
}
private static class ChangeMessageEvent extends Event {
private static final Pattern TOPIC_SET_REGEXP =
Pattern.compile("^Topic set to (.+)$");
private static final Pattern TOPIC_CHANGED_REGEXP =
Pattern.compile("^Topic changed from (.+) to (.+)$");
private static final Pattern TOPIC_REMOVED_REGEXP =
Pattern.compile("^Topic (.+) removed$");
private static final Pattern STATUS_ABANDONED_REGEXP =
Pattern.compile("^Abandoned(\n.*)*$");
private static final Pattern STATUS_RESTORED_REGEXP =
Pattern.compile("^Restored(\n.*)*$");
private final ChangeMessage message;
private final Change noteDbChange;
ChangeMessageEvent(ChangeMessage message, Change noteDbChange,
Timestamp changeCreatedOn) {
super(message.getPatchSetId(), message.getAuthor(),
message.getWrittenOn(), changeCreatedOn, message.getTag());
this.message = message;
this.noteDbChange = noteDbChange;
}
@Override
boolean uniquePerUpdate() {
return true;
}
@Override
void apply(ChangeUpdate update) throws OrmException {
checkUpdate(update);
update.setChangeMessage(message.getMessage());
setTopic(update);
setStatus(update);
}
private void setTopic(ChangeUpdate update) {
String msg = message.getMessage();
if (msg == null) {
return;
}
Matcher m = TOPIC_SET_REGEXP.matcher(msg);
if (m.matches()) {
String topic = m.group(1);
update.setTopic(topic);
noteDbChange.setTopic(topic);
return;
}
m = TOPIC_CHANGED_REGEXP.matcher(msg);
if (m.matches()) {
String topic = m.group(2);
update.setTopic(topic);
noteDbChange.setTopic(topic);
return;
}
if (TOPIC_REMOVED_REGEXP.matcher(msg).matches()) {
update.setTopic(null);
noteDbChange.setTopic(null);
}
}
private void setStatus(ChangeUpdate update) {
String msg = message.getMessage();
if (msg == null) {
return;
}
if (STATUS_ABANDONED_REGEXP.matcher(msg).matches()) {
update.setStatus(Change.Status.ABANDONED);
noteDbChange.setStatus(Change.Status.ABANDONED);
return;
}
if (STATUS_RESTORED_REGEXP.matcher(msg).matches()) {
update.setStatus(Change.Status.NEW);
noteDbChange.setStatus(Change.Status.NEW);
}
}
}
private static class FinalUpdatesEvent extends Event {
private final Change change;
private final Change noteDbChange;
FinalUpdatesEvent(Change change, Change noteDbChange) {
super(change.currentPatchSetId(), change.getOwner(),
change.getLastUpdatedOn(), change.getCreatedOn(), null);
this.change = change;
this.noteDbChange = noteDbChange;
}
@Override
boolean uniquePerUpdate() {
return true;
}
@SuppressWarnings("deprecation")
@Override
void apply(ChangeUpdate update) throws OrmException {
if (!Objects.equals(change.getTopic(), noteDbChange.getTopic())) {
update.setTopic(change.getTopic());
}
if (!Objects.equals(change.getStatus(), noteDbChange.getStatus())) {
// TODO(dborowitz): Stamp approximate approvals at this time.
update.fixStatus(change.getStatus());
}
if (change.getSubmissionId() != null) {
update.setSubmissionId(change.getSubmissionId());
}
if (!update.isEmpty()) {
update.setSubjectForCommit("Final NoteDb migration updates");
}
}
}
}