All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.notedb.ChangeBundle Maven / Gradle / Ivy

There is a newer version: 3.10.0-rc4
Show newest version
// Copyright (C) 2016 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.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSortedMap.toImmutableSortedMap;
import static com.google.gerrit.reviewdb.server.ReviewDbUtil.checkColumns;
import static com.google.gerrit.reviewdb.server.ReviewDbUtil.intKeyOrdering;
import static com.google.gerrit.server.notedb.ChangeBundle.Source.NOTE_DB;
import static com.google.gerrit.server.notedb.ChangeBundle.Source.REVIEW_DB;
import static com.google.gerrit.server.util.time.TimeUtil.truncateToSecond;
import static java.util.Comparator.comparing;
import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.nullsFirst;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;

import com.google.auto.value.AutoValue;
import com.google.common.base.CharMatcher;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
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.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSet.Id;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CommentsUtil;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
import com.google.gwtorm.client.Column;
import com.google.gwtorm.server.OrmException;
import java.lang.reflect.Field;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
 * A bundle of all entities rooted at a single {@link Change} entity.
 *
 * 

See the {@link Change} Javadoc for a depiction of this tree. Bundles may be compared using * {@link #differencesFrom(ChangeBundle)}, which normalizes out the minor implementation differences * between ReviewDb and NoteDb. */ public class ChangeBundle { public enum Source { REVIEW_DB, NOTE_DB; } public static ChangeBundle fromNotes(CommentsUtil commentsUtil, ChangeNotes notes) throws OrmException { return new ChangeBundle( notes.getChange(), notes.getChangeMessages(), notes.getPatchSets().values(), notes.getApprovals().values(), Iterables.concat( CommentsUtil.toPatchLineComments( notes.getChangeId(), PatchLineComment.Status.DRAFT, commentsUtil.draftByChange(null, notes)), CommentsUtil.toPatchLineComments( notes.getChangeId(), PatchLineComment.Status.PUBLISHED, commentsUtil.publishedByChange(null, notes))), notes.getReviewers(), Source.NOTE_DB); } private static ImmutableSortedMap changeMessageMap( Collection in) { return in.stream() .collect( toImmutableSortedMap( comparing((ChangeMessage.Key k) -> k.getParentKey().get()) .thenComparing(k -> k.get()), cm -> cm.getKey(), cm -> cm)); } // Unlike the *Map comparators, which are intended to make key lists diffable, // this comparator sorts first on timestamp, then on every other field. private static final Comparator CHANGE_MESSAGE_COMPARATOR = comparing(ChangeMessage::getWrittenOn) .thenComparing(m -> m.getKey().getParentKey().get()) .thenComparing( m -> m.getPatchSetId() != null ? m.getPatchSetId().get() : null, nullsFirst(naturalOrder())) .thenComparing(ChangeMessage::getAuthor, intKeyOrdering()) .thenComparing(ChangeMessage::getMessage, nullsFirst(naturalOrder())); private static ImmutableList changeMessageList(Iterable in) { return Streams.stream(in).sorted(CHANGE_MESSAGE_COMPARATOR).collect(toImmutableList()); } private static ImmutableSortedMap patchSetMap(Iterable in) { return Streams.stream(in) .collect(toImmutableSortedMap(patchSetIdComparator(), PatchSet::getId, ps -> ps)); } private static ImmutableSortedMap patchSetApprovalMap( Iterable in) { return Streams.stream(in) .collect( toImmutableSortedMap( comparing(PatchSetApproval.Key::getParentKey, patchSetIdComparator()) .thenComparing(PatchSetApproval.Key::getAccountId, intKeyOrdering()) .thenComparing(PatchSetApproval.Key::getLabelId), PatchSetApproval::getKey, a -> a)); } private static ImmutableSortedMap patchLineCommentMap( Iterable in) { return Streams.stream(in) .collect( toImmutableSortedMap( comparing( (PatchLineComment.Key k) -> k.getParentKey().getParentKey(), patchSetIdComparator()) .thenComparing(PatchLineComment.Key::getParentKey) .thenComparing(PatchLineComment.Key::get), PatchLineComment::getKey, c -> c)); } private static Comparator patchSetIdComparator() { return comparing((PatchSet.Id id) -> id.getParentKey().get()).thenComparing(id -> id.get()); } static { // Initialization-time checks that the column set hasn't changed since the // last time this file was updated. checkColumns(Change.Id.class, 1); checkColumns( Change.class, 1, 2, 3, 4, 5, 7, 8, 10, 12, 13, 14, 17, 18, 19, 20, 21, 22, 23, 101); checkColumns(ChangeMessage.Key.class, 1, 2); checkColumns(ChangeMessage.class, 1, 2, 3, 4, 5, 6, 7); checkColumns(PatchSet.Id.class, 1, 2); checkColumns(PatchSet.class, 1, 2, 3, 4, 6, 8, 9); checkColumns(PatchSetApproval.Key.class, 1, 2, 3); checkColumns(PatchSetApproval.class, 1, 2, 3, 6, 7, 8); checkColumns(PatchLineComment.Key.class, 1, 2); checkColumns(PatchLineComment.class, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12); } private final Change change; private final ImmutableList changeMessages; private final ImmutableSortedMap patchSets; private final ImmutableMap patchSetApprovals; private final ImmutableMap patchLineComments; private final ReviewerSet reviewers; private final Source source; public ChangeBundle( Change change, Iterable changeMessages, Iterable patchSets, Iterable patchSetApprovals, Iterable patchLineComments, ReviewerSet reviewers, Source source) { this.change = requireNonNull(change); this.changeMessages = changeMessageList(changeMessages); this.patchSets = ImmutableSortedMap.copyOfSorted(patchSetMap(patchSets)); this.patchSetApprovals = ImmutableMap.copyOf(patchSetApprovalMap(patchSetApprovals)); this.patchLineComments = ImmutableMap.copyOf(patchLineCommentMap(patchLineComments)); this.reviewers = requireNonNull(reviewers); this.source = requireNonNull(source); for (ChangeMessage m : this.changeMessages) { checkArgument(m.getKey().getParentKey().equals(change.getId())); } for (PatchSet.Id id : this.patchSets.keySet()) { checkArgument(id.getParentKey().equals(change.getId())); } for (PatchSetApproval.Key k : this.patchSetApprovals.keySet()) { checkArgument(k.getParentKey().getParentKey().equals(change.getId())); } for (PatchLineComment.Key k : this.patchLineComments.keySet()) { checkArgument(k.getParentKey().getParentKey().getParentKey().equals(change.getId())); } } public Change getChange() { return change; } public ImmutableCollection getChangeMessages() { return changeMessages; } public ImmutableCollection getPatchSets() { return patchSets.values(); } public ImmutableCollection getPatchSetApprovals() { return patchSetApprovals.values(); } public ImmutableCollection getPatchLineComments() { return patchLineComments.values(); } public ReviewerSet getReviewers() { return reviewers; } public Source getSource() { return source; } public ImmutableList differencesFrom(ChangeBundle o) { List diffs = new ArrayList<>(); diffChanges(diffs, this, o); diffChangeMessages(diffs, this, o); diffPatchSets(diffs, this, o); diffPatchSetApprovals(diffs, this, o); diffReviewers(diffs, this, o); diffPatchLineComments(diffs, this, o); return ImmutableList.copyOf(diffs); } private Timestamp getFirstPatchSetTime() { if (patchSets.isEmpty()) { return change.getCreatedOn(); } return patchSets.firstEntry().getValue().getCreatedOn(); } private Timestamp getLatestTimestamp() { Ordering o = Ordering.natural().nullsFirst(); Timestamp ts = null; for (ChangeMessage cm : filterChangeMessages()) { ts = o.max(ts, cm.getWrittenOn()); } for (PatchSet ps : getPatchSets()) { ts = o.max(ts, ps.getCreatedOn()); } for (PatchSetApproval psa : filterPatchSetApprovals().values()) { ts = o.max(ts, psa.getGranted()); } for (PatchLineComment plc : filterPatchLineComments().values()) { // Ignore draft comments, as they do not show up in the change meta graph. if (plc.getStatus() != PatchLineComment.Status.DRAFT) { ts = o.max(ts, plc.getWrittenOn()); } } return firstNonNull(ts, change.getLastUpdatedOn()); } private Map filterPatchSetApprovals() { return limitToValidPatchSets(patchSetApprovals, PatchSetApproval.Key::getParentKey); } private Map filterPatchLineComments() { return limitToValidPatchSets(patchLineComments, k -> k.getParentKey().getParentKey()); } private Map limitToValidPatchSets(Map in, Function func) { return Maps.filterKeys(in, Predicates.compose(validPatchSetPredicate(), func)); } private Predicate validPatchSetPredicate() { return patchSets::containsKey; } private Collection filterChangeMessages() { final Predicate validPatchSet = validPatchSetPredicate(); return Collections2.filter( changeMessages, m -> { PatchSet.Id psId = m.getPatchSetId(); if (psId == null) { return true; } return validPatchSet.apply(psId); }); } private static void diffChanges(List diffs, ChangeBundle bundleA, ChangeBundle bundleB) { Change a = bundleA.change; Change b = bundleB.change; String desc = a.getId().equals(b.getId()) ? describe(a.getId()) : "Changes"; boolean excludeCreatedOn = false; boolean excludeCurrentPatchSetId = false; boolean excludeTopic = false; Timestamp aCreated = a.getCreatedOn(); Timestamp bCreated = b.getCreatedOn(); Timestamp aUpdated = a.getLastUpdatedOn(); Timestamp bUpdated = b.getLastUpdatedOn(); boolean excludeSubject = false; boolean excludeOrigSubj = false; // Subject is not technically a nullable field, but we observed some null // subjects in the wild on googlesource.com, so treat null as empty. String aSubj = Strings.nullToEmpty(a.getSubject()); String bSubj = Strings.nullToEmpty(b.getSubject()); // Allow created timestamp in NoteDb to be any of: // - The created timestamp of the change. // - The timestamp of the first remaining patch set. // - The last updated timestamp, if it is less than the created timestamp. // // Ignore subject if the NoteDb subject starts with the ReviewDb subject. // The NoteDb subject is read directly from the commit, whereas the ReviewDb // subject historically may have been truncated to fit in a SQL varchar // column. // // Ignore original subject on the ReviewDb side when comparing to NoteDb. // This field may have any number of values: // - It may be null, if the change has had no new patch sets pushed since // migrating to schema 103. // - It may match the first patch set subject, if the change was created // after migrating to schema 103. // - It may match the subject of the first patch set that was pushed after // the migration to schema 103, even though that is neither the subject // of the first patch set nor the subject of the last patch set. (See // Change#setCurrentPatchSet as of 43b10f86 for this behavior.) This // subject of an intermediate patch set is not available to the // ChangeBundle; we would have to get the subject from the repo, which is // inconvenient at this point. // // Ignore original subject on the ReviewDb side if it equals the subject of // the current patch set. // // For all of the above subject comparisons, first trim any leading spaces // from the NoteDb strings. (We actually do represent the leading spaces // faithfully during conversion, but JGit's FooterLine parser trims them // when reading.) // // Ignore empty topic on the ReviewDb side if it is null on the NoteDb side. // // Ignore currentPatchSetId on NoteDb side if ReviewDb does not point to a // valid patch set. // // Use max timestamp of all ReviewDb entities when comparing with NoteDb. if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) { boolean createdOnMatchesFirstPs = !timestampsDiffer(bundleA, bundleA.getFirstPatchSetTime(), bundleB, bCreated); boolean createdOnMatchesLastUpdatedOn = !timestampsDiffer(bundleA, aUpdated, bundleB, bCreated); boolean createdAfterUpdated = aCreated.compareTo(aUpdated) > 0; excludeCreatedOn = createdOnMatchesFirstPs || (createdAfterUpdated && createdOnMatchesLastUpdatedOn); aSubj = cleanReviewDbSubject(aSubj); bSubj = cleanNoteDbSubject(bSubj); excludeCurrentPatchSetId = !bundleA.validPatchSetPredicate().apply(a.currentPatchSetId()); excludeSubject = bSubj.startsWith(aSubj) || excludeCurrentPatchSetId; excludeOrigSubj = true; String aTopic = trimOrNull(a.getTopic()); excludeTopic = Objects.equals(aTopic, b.getTopic()) || ("".equals(aTopic) && b.getTopic() == null); aUpdated = bundleA.getLatestTimestamp(); } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) { boolean createdOnMatchesFirstPs = !timestampsDiffer(bundleA, aCreated, bundleB, bundleB.getFirstPatchSetTime()); boolean createdOnMatchesLastUpdatedOn = !timestampsDiffer(bundleA, aCreated, bundleB, bUpdated); boolean createdAfterUpdated = bCreated.compareTo(bUpdated) > 0; excludeCreatedOn = createdOnMatchesFirstPs || (createdAfterUpdated && createdOnMatchesLastUpdatedOn); aSubj = cleanNoteDbSubject(aSubj); bSubj = cleanReviewDbSubject(bSubj); excludeCurrentPatchSetId = !bundleB.validPatchSetPredicate().apply(b.currentPatchSetId()); excludeSubject = aSubj.startsWith(bSubj) || excludeCurrentPatchSetId; excludeOrigSubj = true; String bTopic = trimOrNull(b.getTopic()); excludeTopic = Objects.equals(bTopic, a.getTopic()) || (a.getTopic() == null && "".equals(bTopic)); bUpdated = bundleB.getLatestTimestamp(); } String subjectField = "subject"; String updatedField = "lastUpdatedOn"; List exclude = Lists.newArrayList(subjectField, updatedField, "noteDbState", "rowVersion"); if (excludeCreatedOn) { exclude.add("createdOn"); } if (excludeCurrentPatchSetId) { exclude.add("currentPatchSetId"); } if (excludeOrigSubj) { exclude.add("originalSubject"); } if (excludeTopic) { exclude.add("topic"); } diffColumnsExcluding(diffs, Change.class, desc, bundleA, a, bundleB, b, exclude); // Allow last updated timestamps to either be exactly equal (within slop), // or the NoteDb timestamp to be equal to the latest entity timestamp in the // whole ReviewDb bundle (within slop). if (timestampsDiffer(bundleA, a.getLastUpdatedOn(), bundleB, b.getLastUpdatedOn())) { diffTimestamps( diffs, desc, bundleA, aUpdated, bundleB, bUpdated, "effective last updated time"); } if (!excludeSubject) { diffValues(diffs, desc, aSubj, bSubj, subjectField); } } private static String trimOrNull(String s) { return s != null ? CharMatcher.whitespace().trimFrom(s) : null; } private static String cleanReviewDbSubject(String s) { s = CharMatcher.is(' ').trimLeadingFrom(s); // An old JGit bug failed to extract subjects from commits with "\r\n" // terminators: https://bugs.eclipse.org/bugs/show_bug.cgi?id=400707 // Changes created with this bug may have "\r\n" converted to "\r " and the // entire commit in the subject. The version of JGit used to read NoteDb // changes parses these subjects correctly, so we need to clean up old // ReviewDb subjects before comparing. int rn = s.indexOf("\r \r "); if (rn >= 0) { s = s.substring(0, rn); } return NoteDbUtil.sanitizeFooter(s); } private static String cleanNoteDbSubject(String s) { return NoteDbUtil.sanitizeFooter(s); } /** * Set of fields that must always exactly match between ReviewDb and NoteDb. * *

Used to limit the worst-case quadratic search when pairing off matching messages below. */ @AutoValue abstract static class ChangeMessageCandidate { static ChangeMessageCandidate create(ChangeMessage cm) { return new AutoValue_ChangeBundle_ChangeMessageCandidate( cm.getAuthor(), cm.getMessage(), cm.getTag()); } @Nullable abstract Account.Id author(); @Nullable abstract String message(); @Nullable abstract String tag(); // Exclude: // - patch set, which may be null on ReviewDb side but not NoteDb // - UUID, which is always different between ReviewDb and NoteDb // - writtenOn, which is fuzzy } private static void diffChangeMessages( List diffs, ChangeBundle bundleA, ChangeBundle bundleB) { if (bundleA.source == REVIEW_DB && bundleB.source == REVIEW_DB) { // Both came from ReviewDb: check all fields exactly. Map as = changeMessageMap(bundleA.filterChangeMessages()); Map bs = changeMessageMap(bundleB.filterChangeMessages()); for (ChangeMessage.Key k : diffKeySets(diffs, as, bs)) { ChangeMessage a = as.get(k); ChangeMessage b = bs.get(k); String desc = describe(k); diffColumns(diffs, ChangeMessage.class, desc, bundleA, a, bundleB, b); } return; } Change.Id id = bundleA.getChange().getId(); checkArgument(id.equals(bundleB.getChange().getId())); // Try to pair up matching ChangeMessages from each side, and succeed only // if both collections are empty at the end. Quadratic in the worst case, // but easy to reason about. List as = new LinkedList<>(bundleA.filterChangeMessages()); ListMultimap bs = LinkedListMultimap.create(); for (ChangeMessage b : bundleB.filterChangeMessages()) { bs.put(ChangeMessageCandidate.create(b), b); } Iterator ait = as.iterator(); A: while (ait.hasNext()) { ChangeMessage a = ait.next(); Iterator bit = bs.get(ChangeMessageCandidate.create(a)).iterator(); while (bit.hasNext()) { ChangeMessage b = bit.next(); if (changeMessagesMatch(bundleA, a, bundleB, b)) { ait.remove(); bit.remove(); continue A; } } } if (as.isEmpty() && bs.isEmpty()) { return; } StringBuilder sb = new StringBuilder("ChangeMessages differ for Change.Id ").append(id).append('\n'); if (!as.isEmpty()) { sb.append("Only in A:"); for (ChangeMessage cm : as) { sb.append("\n ").append(cm); } if (!bs.isEmpty()) { sb.append('\n'); } } if (!bs.isEmpty()) { sb.append("Only in B:"); bs.values() .stream() .sorted(CHANGE_MESSAGE_COMPARATOR) .forEach(cm -> sb.append("\n ").append(cm)); } diffs.add(sb.toString()); } private static boolean changeMessagesMatch( ChangeBundle bundleA, ChangeMessage a, ChangeBundle bundleB, ChangeMessage b) { List tempDiffs = new ArrayList<>(); String temp = "temp"; // ReviewDb allows timestamps before patch set was created, but NoteDb // truncates this to the patch set creation timestamp. Timestamp ta = a.getWrittenOn(); Timestamp tb = b.getWrittenOn(); PatchSet psa = bundleA.patchSets.get(a.getPatchSetId()); PatchSet psb = bundleB.patchSets.get(b.getPatchSetId()); boolean excludePatchSet = false; boolean excludeWrittenOn = false; if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) { excludePatchSet = a.getPatchSetId() == null; excludeWrittenOn = psa != null && psb != null && ta.before(psa.getCreatedOn()) && tb.equals(psb.getCreatedOn()); } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) { excludePatchSet = b.getPatchSetId() == null; excludeWrittenOn = psa != null && psb != null && tb.before(psb.getCreatedOn()) && ta.equals(psa.getCreatedOn()); } List exclude = Lists.newArrayList("key"); if (excludePatchSet) { exclude.add("patchset"); } if (excludeWrittenOn) { exclude.add("writtenOn"); } diffColumnsExcluding(tempDiffs, ChangeMessage.class, temp, bundleA, a, bundleB, b, exclude); return tempDiffs.isEmpty(); } private static void diffPatchSets( List diffs, ChangeBundle bundleA, ChangeBundle bundleB) { Map as = bundleA.patchSets; Map bs = bundleB.patchSets; Optional minA = as.keySet().stream().min(intKeyOrdering()); Optional minB = bs.keySet().stream().min(intKeyOrdering()); Set ids = diffKeySets(diffs, as, bs); // Old versions of Gerrit had a bug that created patch sets during // rebase or submission with a createdOn timestamp earlier than the patch // set it was replacing. (In the cases I examined, it was equal to createdOn // for the change, but we're not counting on this exact behavior.) // // ChangeRebuilder ensures patch set events come out in order, but it's hard // to predict what the resulting timestamps would look like. So, completely // ignore the createdOn timestamps if both: // * ReviewDb timestamps are non-monotonic. // * NoteDb timestamps are monotonic. // // Allow the timestamp of the first patch set to match the creation time of // the change. boolean excludeAllCreatedOn = false; if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) { excludeAllCreatedOn = !createdOnIsMonotonic(as, ids) && createdOnIsMonotonic(bs, ids); } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) { excludeAllCreatedOn = createdOnIsMonotonic(as, ids) && !createdOnIsMonotonic(bs, ids); } for (PatchSet.Id id : ids) { PatchSet a = as.get(id); PatchSet b = bs.get(id); String desc = describe(id); String pushCertField = "pushCertificate"; boolean excludeCreatedOn = excludeAllCreatedOn; boolean excludeDesc = false; if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) { excludeDesc = Objects.equals(trimOrNull(a.getDescription()), b.getDescription()); excludeCreatedOn |= Optional.of(id).equals(minB) && b.getCreatedOn().equals(bundleB.change.getCreatedOn()); } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) { excludeDesc = Objects.equals(a.getDescription(), trimOrNull(b.getDescription())); excludeCreatedOn |= Optional.of(id).equals(minA) && a.getCreatedOn().equals(bundleA.change.getCreatedOn()); } List exclude = Lists.newArrayList(pushCertField); if (excludeCreatedOn) { exclude.add("createdOn"); } if (excludeDesc) { exclude.add("description"); } diffColumnsExcluding(diffs, PatchSet.class, desc, bundleA, a, bundleB, b, exclude); diffValues(diffs, desc, trimPushCert(a), trimPushCert(b), pushCertField); } } private static String trimPushCert(PatchSet ps) { if (ps.getPushCertificate() == null) { return null; } return CharMatcher.is('\n').trimTrailingFrom(ps.getPushCertificate()); } private static boolean createdOnIsMonotonic( Map patchSets, Set limitToIds) { List orderedById = patchSets .values() .stream() .filter(ps -> limitToIds.contains(ps.getId())) .sorted(ChangeUtil.PS_ID_ORDER) .collect(toList()); return Ordering.natural().onResultOf(PatchSet::getCreatedOn).isOrdered(orderedById); } private static void diffPatchSetApprovals( List diffs, ChangeBundle bundleA, ChangeBundle bundleB) { Map as = bundleA.filterPatchSetApprovals(); Map bs = bundleB.filterPatchSetApprovals(); for (PatchSetApproval.Key k : diffKeySets(diffs, as, bs)) { PatchSetApproval a = as.get(k); PatchSetApproval b = bs.get(k); String desc = describe(k); // ReviewDb allows timestamps before patch set was created, but NoteDb // truncates this to the patch set creation timestamp. // // ChangeRebuilder ensures all post-submit approvals happen after the // actual submit, so the timestamps may not line up. This shouldn't really // happen, because postSubmit shouldn't be set in ReviewDb until after the // change is submitted in ReviewDb, but you never know. // // Due to a quirk of PostReview, post-submit 0 votes might not have the // postSubmit bit set in ReviewDb. As these are only used for tombstone // purposes, ignore the postSubmit bit in NoteDb in this case. Timestamp ta = a.getGranted(); Timestamp tb = b.getGranted(); PatchSet psa = requireNonNull(bundleA.patchSets.get(a.getPatchSetId())); PatchSet psb = requireNonNull(bundleB.patchSets.get(b.getPatchSetId())); boolean excludeGranted = false; boolean excludePostSubmit = false; List exclude = new ArrayList<>(1); if (bundleA.source == REVIEW_DB && bundleB.source == NOTE_DB) { excludeGranted = (ta.before(psa.getCreatedOn()) && tb.equals(psb.getCreatedOn())) || ta.compareTo(tb) < 0; excludePostSubmit = a.getValue() == 0 && b.isPostSubmit(); } else if (bundleA.source == NOTE_DB && bundleB.source == REVIEW_DB) { excludeGranted = (tb.before(psb.getCreatedOn()) && ta.equals(psa.getCreatedOn())) || (tb.compareTo(ta) < 0); excludePostSubmit = b.getValue() == 0 && a.isPostSubmit(); } // Legacy submit approvals may or may not have tags associated with them, // depending on whether ChangeRebuilder happened to group them with the // status change. boolean excludeTag = bundleA.source != bundleB.source && a.isLegacySubmit() && b.isLegacySubmit(); if (excludeGranted) { exclude.add("granted"); } if (excludePostSubmit) { exclude.add("postSubmit"); } if (excludeTag) { exclude.add("tag"); } diffColumnsExcluding(diffs, PatchSetApproval.class, desc, bundleA, a, bundleB, b, exclude); } } private static void diffReviewers( List diffs, ChangeBundle bundleA, ChangeBundle bundleB) { diffSets(diffs, bundleA.reviewers.all(), bundleB.reviewers.all(), "reviewer"); } private static void diffPatchLineComments( List diffs, ChangeBundle bundleA, ChangeBundle bundleB) { Map as = bundleA.filterPatchLineComments(); Map bs = bundleB.filterPatchLineComments(); for (PatchLineComment.Key k : diffKeySets(diffs, as, bs)) { PatchLineComment a = as.get(k); PatchLineComment b = bs.get(k); String desc = describe(k); diffColumns(diffs, PatchLineComment.class, desc, bundleA, a, bundleB, b); } } private static Set diffKeySets(List diffs, Map a, Map b) { if (a.isEmpty() && b.isEmpty()) { return a.keySet(); } String clazz = keyClass((!a.isEmpty() ? a.keySet() : b.keySet()).iterator().next()); return diffSets(diffs, a.keySet(), b.keySet(), clazz); } private static Set diffSets(List diffs, Set as, Set bs, String desc) { if (as.isEmpty() && bs.isEmpty()) { return as; } Set aNotB = Sets.difference(as, bs); Set bNotA = Sets.difference(bs, as); if (aNotB.isEmpty() && bNotA.isEmpty()) { return as; } diffs.add(desc + " sets differ: " + aNotB + " only in A; " + bNotA + " only in B"); return Sets.intersection(as, bs); } private static void diffColumns( List diffs, Class clazz, String desc, ChangeBundle bundleA, T a, ChangeBundle bundleB, T b) { diffColumnsExcluding(diffs, clazz, desc, bundleA, a, bundleB, b); } private static void diffColumnsExcluding( List diffs, Class clazz, String desc, ChangeBundle bundleA, T a, ChangeBundle bundleB, T b, String... exclude) { diffColumnsExcluding(diffs, clazz, desc, bundleA, a, bundleB, b, Arrays.asList(exclude)); } private static void diffColumnsExcluding( List diffs, Class clazz, String desc, ChangeBundle bundleA, T a, ChangeBundle bundleB, T b, Iterable exclude) { Set toExclude = Sets.newLinkedHashSet(exclude); for (Field f : clazz.getDeclaredFields()) { Column col = f.getAnnotation(Column.class); if (col == null) { continue; } else if (toExclude.remove(f.getName())) { continue; } f.setAccessible(true); try { if (Timestamp.class.isAssignableFrom(f.getType())) { diffTimestamps(diffs, desc, bundleA, a, bundleB, b, f.getName()); } else { diffValues(diffs, desc, f.get(a), f.get(b), f.getName()); } } catch (IllegalAccessException e) { throw new IllegalArgumentException(e); } } checkArgument( toExclude.isEmpty(), "requested columns to exclude not present in %s: %s", clazz.getSimpleName(), toExclude); } private static void diffTimestamps( List diffs, String desc, ChangeBundle bundleA, Object a, ChangeBundle bundleB, Object b, String field) { checkArgument(a.getClass() == b.getClass()); Class clazz = a.getClass(); Timestamp ta; Timestamp tb; try { Field f = clazz.getDeclaredField(field); checkArgument(f.getAnnotation(Column.class) != null); f.setAccessible(true); ta = (Timestamp) f.get(a); tb = (Timestamp) f.get(b); } catch (IllegalAccessException | NoSuchFieldException | SecurityException e) { throw new IllegalArgumentException(e); } diffTimestamps(diffs, desc, bundleA, ta, bundleB, tb, field); } private static void diffTimestamps( List diffs, String desc, ChangeBundle bundleA, Timestamp ta, ChangeBundle bundleB, Timestamp tb, String fieldDesc) { if (bundleA.source == bundleB.source || ta == null || tb == null) { diffValues(diffs, desc, ta, tb, fieldDesc); } else if (bundleA.source == NOTE_DB) { diffTimestamps(diffs, desc, bundleA.getChange(), ta, bundleB.getChange(), tb, fieldDesc); } else { diffTimestamps(diffs, desc, bundleB.getChange(), tb, bundleA.getChange(), ta, fieldDesc); } } private static boolean timestampsDiffer( ChangeBundle bundleA, Timestamp ta, ChangeBundle bundleB, Timestamp tb) { List tempDiffs = new ArrayList<>(1); diffTimestamps(tempDiffs, "temp", bundleA, ta, bundleB, tb, "temp"); return !tempDiffs.isEmpty(); } private static void diffTimestamps( List diffs, String desc, Change changeFromNoteDb, Timestamp tsFromNoteDb, Change changeFromReviewDb, Timestamp tsFromReviewDb, String field) { // Because ChangeRebuilder may batch events together that are several // seconds apart, the timestamp in NoteDb may actually be several seconds // *earlier* than the timestamp in ReviewDb that it was converted from. checkArgument( tsFromNoteDb.equals(truncateToSecond(tsFromNoteDb)), "%s from NoteDb has non-rounded %s timestamp: %s", desc, field, tsFromNoteDb); if (tsFromReviewDb.before(changeFromReviewDb.getCreatedOn()) && tsFromNoteDb.equals(changeFromNoteDb.getCreatedOn())) { // Timestamp predates change creation. These are truncated to change // creation time during NoteDb conversion, so allow this if the timestamp // in NoteDb matches the createdOn time in NoteDb. return; } long delta = tsFromReviewDb.getTime() - tsFromNoteDb.getTime(); long max = ChangeRebuilderImpl.MAX_WINDOW_MS; if (delta < 0 || delta > max) { diffs.add( field + " differs for " + desc + " in NoteDb vs. ReviewDb:" + " {" + tsFromNoteDb + "} != {" + tsFromReviewDb + "}"); } } private static void diffValues( List diffs, String desc, Object va, Object vb, String name) { if (!Objects.equals(va, vb)) { diffs.add(name + " differs for " + desc + ": {" + va + "} != {" + vb + "}"); } } private static String describe(Object key) { return keyClass(key) + " " + key; } private static String keyClass(Object obj) { Class clazz = obj.getClass(); String name = clazz.getSimpleName(); checkArgument(name.endsWith("Key") || name.endsWith("Id"), "not an Id/Key class: %s", name); if (name.equals("Key") || name.equals("Id")) { return clazz.getEnclosingClass().getSimpleName() + "." + name; } else if (name.startsWith("AutoValue_")) { return name.substring(name.lastIndexOf('_') + 1); } return name; } @Override public String toString() { return getClass().getSimpleName() + "{id=" + change.getId() + ", ChangeMessage[" + changeMessages.size() + "]" + ", PatchSet[" + patchSets.size() + "]" + ", PatchSetApproval[" + patchSetApprovals.size() + "]" + ", PatchLineComment[" + patchLineComments.size() + "]" + "}"; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy