Remove remaining NoteDb rebuilding machinery
Now that we no longer auto-rebuild changes and we have no ReviewDb to NoteDb migration code, the rebuild package and associated code is completely unused. Change-Id: Id4d65ce2bcd6ba828cc39addfe1467fae738eb6b
This commit is contained in:
@@ -28,8 +28,6 @@ import com.google.gerrit.server.config.SitePaths;
|
|||||||
import com.google.gerrit.server.config.TrackingFooters;
|
import com.google.gerrit.server.config.TrackingFooters;
|
||||||
import com.google.gerrit.server.config.TrackingFootersProvider;
|
import com.google.gerrit.server.config.TrackingFootersProvider;
|
||||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
import com.google.gerrit.server.git.GitRepositoryManager;
|
||||||
import com.google.gerrit.server.notedb.ChangeBundleReader;
|
|
||||||
import com.google.gerrit.server.notedb.GwtormChangeBundleReader;
|
|
||||||
import com.google.gerrit.server.notedb.NotesMigration;
|
import com.google.gerrit.server.notedb.NotesMigration;
|
||||||
import com.google.gerrit.server.schema.NotesMigrationSchemaFactory;
|
import com.google.gerrit.server.schema.NotesMigrationSchemaFactory;
|
||||||
import com.google.gerrit.server.schema.ReviewDbFactory;
|
import com.google.gerrit.server.schema.ReviewDbFactory;
|
||||||
@@ -83,7 +81,6 @@ class InMemoryTestingDatabaseModule extends LifecycleModule {
|
|||||||
bind(schemaFactory).to(NotesMigrationSchemaFactory.class);
|
bind(schemaFactory).to(NotesMigrationSchemaFactory.class);
|
||||||
bind(Key.get(schemaFactory, ReviewDbFactory.class)).to(InMemoryDatabase.class);
|
bind(Key.get(schemaFactory, ReviewDbFactory.class)).to(InMemoryDatabase.class);
|
||||||
bind(InMemoryDatabase.class).in(SINGLETON);
|
bind(InMemoryDatabase.class).in(SINGLETON);
|
||||||
bind(ChangeBundleReader.class).to(GwtormChangeBundleReader.class);
|
|
||||||
|
|
||||||
listener().to(CreateDatabase.class);
|
listener().to(CreateDatabase.class);
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ import com.google.gerrit.server.config.CanonicalWebUrlProvider;
|
|||||||
import com.google.gerrit.server.config.DefaultUrlFormatter;
|
import com.google.gerrit.server.config.DefaultUrlFormatter;
|
||||||
import com.google.gerrit.server.config.DisableReverseDnsLookup;
|
import com.google.gerrit.server.config.DisableReverseDnsLookup;
|
||||||
import com.google.gerrit.server.config.DisableReverseDnsLookupProvider;
|
import com.google.gerrit.server.config.DisableReverseDnsLookupProvider;
|
||||||
import com.google.gerrit.server.config.GerritServerConfig;
|
|
||||||
import com.google.gerrit.server.config.GitReceivePackGroups;
|
import com.google.gerrit.server.config.GitReceivePackGroups;
|
||||||
import com.google.gerrit.server.config.GitUploadPackGroups;
|
import com.google.gerrit.server.config.GitUploadPackGroups;
|
||||||
import com.google.gerrit.server.config.SysExecutorModule;
|
import com.google.gerrit.server.config.SysExecutorModule;
|
||||||
@@ -87,16 +86,13 @@ import com.google.inject.util.Providers;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.eclipse.jgit.lib.Config;
|
|
||||||
|
|
||||||
/** Module for programs that perform batch operations on a site. */
|
/** Module for programs that perform batch operations on a site. */
|
||||||
public class BatchProgramModule extends FactoryModule {
|
public class BatchProgramModule extends FactoryModule {
|
||||||
private final Config cfg;
|
|
||||||
private final Module reviewDbModule;
|
private final Module reviewDbModule;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
BatchProgramModule(@GerritServerConfig Config cfg, PerThreadReviewDbModule reviewDbModule) {
|
BatchProgramModule(PerThreadReviewDbModule reviewDbModule) {
|
||||||
this.cfg = cfg;
|
|
||||||
this.reviewDbModule = reviewDbModule;
|
this.reviewDbModule = reviewDbModule;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +164,7 @@ public class BatchProgramModule extends FactoryModule {
|
|||||||
install(new H2CacheModule());
|
install(new H2CacheModule());
|
||||||
install(new ExternalIdModule());
|
install(new ExternalIdModule());
|
||||||
install(new GroupModule());
|
install(new GroupModule());
|
||||||
install(new NoteDbModule(cfg));
|
install(new NoteDbModule());
|
||||||
install(AccountCacheImpl.module());
|
install(AccountCacheImpl.module());
|
||||||
install(GroupCacheImpl.module());
|
install(GroupCacheImpl.module());
|
||||||
install(GroupIncludeCacheImpl.module());
|
install(GroupIncludeCacheImpl.module());
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ public class GerritGlobalModule extends FactoryModule {
|
|||||||
install(new GitModule());
|
install(new GitModule());
|
||||||
install(new GroupDbModule());
|
install(new GroupDbModule());
|
||||||
install(new GroupModule());
|
install(new GroupModule());
|
||||||
install(new NoteDbModule(cfg));
|
install(new NoteDbModule());
|
||||||
install(new PrologModule());
|
install(new PrologModule());
|
||||||
install(new DefaultSubmitRule.Module());
|
install(new DefaultSubmitRule.Module());
|
||||||
install(new IgnoreSelfApprovalRule.Module());
|
install(new IgnoreSelfApprovalRule.Module());
|
||||||
|
|||||||
@@ -1,976 +0,0 @@
|
|||||||
// 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.
|
|
||||||
*
|
|
||||||
* <p>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<ChangeMessage.Key, ChangeMessage> changeMessageMap(
|
|
||||||
Collection<ChangeMessage> 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<ChangeMessage> 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<ChangeMessage> changeMessageList(Iterable<ChangeMessage> in) {
|
|
||||||
return Streams.stream(in).sorted(CHANGE_MESSAGE_COMPARATOR).collect(toImmutableList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ImmutableSortedMap<Id, PatchSet> patchSetMap(Iterable<PatchSet> in) {
|
|
||||||
return Streams.stream(in)
|
|
||||||
.collect(toImmutableSortedMap(patchSetIdComparator(), PatchSet::getId, ps -> ps));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ImmutableSortedMap<PatchSetApproval.Key, PatchSetApproval> patchSetApprovalMap(
|
|
||||||
Iterable<PatchSetApproval> 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<PatchLineComment.Key, PatchLineComment> patchLineCommentMap(
|
|
||||||
Iterable<PatchLineComment> 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<PatchSet.Id> 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<ChangeMessage> changeMessages;
|
|
||||||
private final ImmutableSortedMap<PatchSet.Id, PatchSet> patchSets;
|
|
||||||
private final ImmutableMap<PatchSetApproval.Key, PatchSetApproval> patchSetApprovals;
|
|
||||||
private final ImmutableMap<PatchLineComment.Key, PatchLineComment> patchLineComments;
|
|
||||||
private final ReviewerSet reviewers;
|
|
||||||
private final Source source;
|
|
||||||
|
|
||||||
public ChangeBundle(
|
|
||||||
Change change,
|
|
||||||
Iterable<ChangeMessage> changeMessages,
|
|
||||||
Iterable<PatchSet> patchSets,
|
|
||||||
Iterable<PatchSetApproval> patchSetApprovals,
|
|
||||||
Iterable<PatchLineComment> 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<ChangeMessage> getChangeMessages() {
|
|
||||||
return changeMessages;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ImmutableCollection<PatchSet> getPatchSets() {
|
|
||||||
return patchSets.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ImmutableCollection<PatchSetApproval> getPatchSetApprovals() {
|
|
||||||
return patchSetApprovals.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ImmutableCollection<PatchLineComment> getPatchLineComments() {
|
|
||||||
return patchLineComments.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ReviewerSet getReviewers() {
|
|
||||||
return reviewers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Source getSource() {
|
|
||||||
return source;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ImmutableList<String> differencesFrom(ChangeBundle o) {
|
|
||||||
List<String> 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<Timestamp> 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<PatchSetApproval.Key, PatchSetApproval> filterPatchSetApprovals() {
|
|
||||||
return limitToValidPatchSets(patchSetApprovals, PatchSetApproval.Key::getParentKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Map<PatchLineComment.Key, PatchLineComment> filterPatchLineComments() {
|
|
||||||
return limitToValidPatchSets(patchLineComments, k -> k.getParentKey().getParentKey());
|
|
||||||
}
|
|
||||||
|
|
||||||
private <K, V> Map<K, V> limitToValidPatchSets(Map<K, V> in, Function<K, PatchSet.Id> func) {
|
|
||||||
return Maps.filterKeys(in, Predicates.compose(validPatchSetPredicate(), func));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Predicate<PatchSet.Id> validPatchSetPredicate() {
|
|
||||||
return patchSets::containsKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Collection<ChangeMessage> filterChangeMessages() {
|
|
||||||
final Predicate<PatchSet.Id> 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<String> 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<String> 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.
|
|
||||||
*
|
|
||||||
* <p>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<String> diffs, ChangeBundle bundleA, ChangeBundle bundleB) {
|
|
||||||
if (bundleA.source == REVIEW_DB && bundleB.source == REVIEW_DB) {
|
|
||||||
// Both came from ReviewDb: check all fields exactly.
|
|
||||||
Map<ChangeMessage.Key, ChangeMessage> as = changeMessageMap(bundleA.filterChangeMessages());
|
|
||||||
Map<ChangeMessage.Key, ChangeMessage> 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<ChangeMessage> as = new LinkedList<>(bundleA.filterChangeMessages());
|
|
||||||
|
|
||||||
ListMultimap<ChangeMessageCandidate, ChangeMessage> bs = LinkedListMultimap.create();
|
|
||||||
for (ChangeMessage b : bundleB.filterChangeMessages()) {
|
|
||||||
bs.put(ChangeMessageCandidate.create(b), b);
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterator<ChangeMessage> ait = as.iterator();
|
|
||||||
A:
|
|
||||||
while (ait.hasNext()) {
|
|
||||||
ChangeMessage a = ait.next();
|
|
||||||
Iterator<ChangeMessage> 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<String> 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<String> 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<String> diffs, ChangeBundle bundleA, ChangeBundle bundleB) {
|
|
||||||
Map<PatchSet.Id, PatchSet> as = bundleA.patchSets;
|
|
||||||
Map<PatchSet.Id, PatchSet> bs = bundleB.patchSets;
|
|
||||||
Optional<PatchSet.Id> minA = as.keySet().stream().min(intKeyOrdering());
|
|
||||||
Optional<PatchSet.Id> minB = bs.keySet().stream().min(intKeyOrdering());
|
|
||||||
Set<PatchSet.Id> 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<String> 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<?, PatchSet> patchSets, Set<PatchSet.Id> limitToIds) {
|
|
||||||
List<PatchSet> 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<String> diffs, ChangeBundle bundleA, ChangeBundle bundleB) {
|
|
||||||
Map<PatchSetApproval.Key, PatchSetApproval> as = bundleA.filterPatchSetApprovals();
|
|
||||||
Map<PatchSetApproval.Key, PatchSetApproval> 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<String> 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<String> diffs, ChangeBundle bundleA, ChangeBundle bundleB) {
|
|
||||||
diffSets(diffs, bundleA.reviewers.all(), bundleB.reviewers.all(), "reviewer");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void diffPatchLineComments(
|
|
||||||
List<String> diffs, ChangeBundle bundleA, ChangeBundle bundleB) {
|
|
||||||
Map<PatchLineComment.Key, PatchLineComment> as = bundleA.filterPatchLineComments();
|
|
||||||
Map<PatchLineComment.Key, PatchLineComment> 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 <T> Set<T> diffKeySets(List<String> diffs, Map<T, ?> a, Map<T, ?> 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 <T> Set<T> diffSets(List<String> diffs, Set<T> as, Set<T> bs, String desc) {
|
|
||||||
if (as.isEmpty() && bs.isEmpty()) {
|
|
||||||
return as;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<T> aNotB = Sets.difference(as, bs);
|
|
||||||
Set<T> 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 <T> void diffColumns(
|
|
||||||
List<String> diffs,
|
|
||||||
Class<T> clazz,
|
|
||||||
String desc,
|
|
||||||
ChangeBundle bundleA,
|
|
||||||
T a,
|
|
||||||
ChangeBundle bundleB,
|
|
||||||
T b) {
|
|
||||||
diffColumnsExcluding(diffs, clazz, desc, bundleA, a, bundleB, b);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <T> void diffColumnsExcluding(
|
|
||||||
List<String> diffs,
|
|
||||||
Class<T> 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 <T> void diffColumnsExcluding(
|
|
||||||
List<String> diffs,
|
|
||||||
Class<T> clazz,
|
|
||||||
String desc,
|
|
||||||
ChangeBundle bundleA,
|
|
||||||
T a,
|
|
||||||
ChangeBundle bundleB,
|
|
||||||
T b,
|
|
||||||
Iterable<String> exclude) {
|
|
||||||
Set<String> 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<String> 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<String> 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<String> tempDiffs = new ArrayList<>(1);
|
|
||||||
diffTimestamps(tempDiffs, "temp", bundleA, ta, bundleB, tb, "temp");
|
|
||||||
return !tempDiffs.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void diffTimestamps(
|
|
||||||
List<String> 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<String> 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()
|
|
||||||
+ "]"
|
|
||||||
+ "}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// 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 com.google.gerrit.common.Nullable;
|
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
|
|
||||||
public interface ChangeBundleReader {
|
|
||||||
@Nullable
|
|
||||||
ChangeBundle fromReviewDb(ReviewDb db, Change.Id id) throws OrmException;
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
// 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 com.google.gerrit.common.Nullable;
|
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
|
||||||
import com.google.gerrit.reviewdb.client.PatchSetApproval;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
|
||||||
import com.google.gerrit.server.ReviewerSet;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeBundle.Source;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import com.google.inject.Singleton;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class GwtormChangeBundleReader implements ChangeBundleReader {
|
|
||||||
@Inject
|
|
||||||
GwtormChangeBundleReader() {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Nullable
|
|
||||||
public ChangeBundle fromReviewDb(ReviewDb db, Change.Id id) throws OrmException {
|
|
||||||
Change reviewDbChange = db.changes().get(id);
|
|
||||||
if (reviewDbChange == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(dborowitz): Figure out how to do this more consistently, e.g. hand-written inner joins.
|
|
||||||
List<PatchSetApproval> approvals = db.patchSetApprovals().byChange(id).toList();
|
|
||||||
return new ChangeBundle(
|
|
||||||
reviewDbChange,
|
|
||||||
db.changeMessages().byChange(id),
|
|
||||||
db.patchSets().byChange(id),
|
|
||||||
approvals,
|
|
||||||
db.patchComments().byChange(id),
|
|
||||||
ReviewerSet.fromApprovals(approvals),
|
|
||||||
Source.REVIEW_DB);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,31 +17,21 @@ package com.google.gerrit.server.notedb;
|
|||||||
import com.google.common.cache.Cache;
|
import com.google.common.cache.Cache;
|
||||||
import com.google.common.cache.CacheBuilder;
|
import com.google.common.cache.CacheBuilder;
|
||||||
import com.google.gerrit.extensions.config.FactoryModule;
|
import com.google.gerrit.extensions.config.FactoryModule;
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
|
||||||
import com.google.gerrit.reviewdb.client.Change.Id;
|
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
|
||||||
import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
|
|
||||||
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
|
|
||||||
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
|
|
||||||
import com.google.inject.TypeLiteral;
|
import com.google.inject.TypeLiteral;
|
||||||
import com.google.inject.name.Names;
|
import com.google.inject.name.Names;
|
||||||
import org.eclipse.jgit.lib.Config;
|
|
||||||
|
|
||||||
public class NoteDbModule extends FactoryModule {
|
public class NoteDbModule extends FactoryModule {
|
||||||
private final Config cfg;
|
|
||||||
private final boolean useTestBindings;
|
private final boolean useTestBindings;
|
||||||
|
|
||||||
static NoteDbModule forTest(Config cfg) {
|
static NoteDbModule forTest() {
|
||||||
return new NoteDbModule(cfg, true);
|
return new NoteDbModule(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public NoteDbModule(Config cfg) {
|
public NoteDbModule() {
|
||||||
this(cfg, false);
|
this(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private NoteDbModule(Config cfg, boolean useTestBindings) {
|
private NoteDbModule(boolean useTestBindings) {
|
||||||
this.cfg = cfg;
|
|
||||||
this.useTestBindings = useTestBindings;
|
this.useTestBindings = useTestBindings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,53 +47,7 @@ public class NoteDbModule extends FactoryModule {
|
|||||||
|
|
||||||
if (!useTestBindings) {
|
if (!useTestBindings) {
|
||||||
install(ChangeNotesCache.module());
|
install(ChangeNotesCache.module());
|
||||||
if (cfg.getBoolean("noteDb", null, "testRebuilderWrapper", false)) {
|
|
||||||
// Yes, another variety of test bindings with a different way of
|
|
||||||
// configuring it.
|
|
||||||
bind(ChangeRebuilder.class).to(TestChangeRebuilderWrapper.class);
|
|
||||||
} else {
|
|
||||||
bind(ChangeRebuilder.class).to(ChangeRebuilderImpl.class);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
bind(ChangeRebuilder.class)
|
|
||||||
.toInstance(
|
|
||||||
new ChangeRebuilder(null) {
|
|
||||||
@Override
|
|
||||||
public Result rebuild(ReviewDb db, Change.Id changeId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Result rebuildEvenIfReadOnly(ReviewDb db, Id changeId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Result rebuild(NoteDbUpdateManager manager, ChangeBundle bundle) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Result execute(
|
|
||||||
ReviewDb db, Change.Id changeId, NoteDbUpdateManager manager) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void rebuildReviewDb(ReviewDb db, Project.NameKey project, Id changeId) {
|
|
||||||
// Do nothing.
|
|
||||||
}
|
|
||||||
});
|
|
||||||
bind(new TypeLiteral<Cache<ChangeNotesCache.Key, ChangeNotesState>>() {})
|
bind(new TypeLiteral<Cache<ChangeNotesCache.Key, ChangeNotesState>>() {})
|
||||||
.annotatedWith(Names.named(ChangeNotesCache.CACHE_NAME))
|
.annotatedWith(Names.named(ChangeNotesCache.CACHE_NAME))
|
||||||
.toInstance(CacheBuilder.newBuilder().<ChangeNotesCache.Key, ChangeNotesState>build());
|
.toInstance(CacheBuilder.newBuilder().<ChangeNotesCache.Key, ChangeNotesState>build());
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
// 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 com.google.common.annotations.VisibleForTesting;
|
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
|
||||||
import com.google.gerrit.reviewdb.client.Change.Id;
|
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
|
||||||
import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
|
|
||||||
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
|
|
||||||
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
import com.google.gwtorm.server.SchemaFactory;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import com.google.inject.Singleton;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
@Singleton
|
|
||||||
public class TestChangeRebuilderWrapper extends ChangeRebuilder {
|
|
||||||
private final ChangeRebuilderImpl delegate;
|
|
||||||
private final AtomicBoolean failNextUpdate;
|
|
||||||
private final AtomicBoolean stealNextUpdate;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
TestChangeRebuilderWrapper(SchemaFactory<ReviewDb> schemaFactory, ChangeRebuilderImpl rebuilder) {
|
|
||||||
super(schemaFactory);
|
|
||||||
this.delegate = rebuilder;
|
|
||||||
this.failNextUpdate = new AtomicBoolean();
|
|
||||||
this.stealNextUpdate = new AtomicBoolean();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void failNextUpdate() {
|
|
||||||
failNextUpdate.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stealNextUpdate() {
|
|
||||||
stealNextUpdate.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 {
|
|
||||||
if (failNextUpdate.getAndSet(false)) {
|
|
||||||
throw new IOException("Update failed");
|
|
||||||
}
|
|
||||||
Result result =
|
|
||||||
checkReadOnly
|
|
||||||
? delegate.rebuild(db, changeId)
|
|
||||||
: delegate.rebuildEvenIfReadOnly(db, changeId);
|
|
||||||
if (stealNextUpdate.getAndSet(false)) {
|
|
||||||
throw new IOException("Update stolen");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Result rebuild(NoteDbUpdateManager manager, ChangeBundle bundle)
|
|
||||||
throws IOException, OrmException {
|
|
||||||
// stealNextUpdate doesn't really apply in this case because the IOException
|
|
||||||
// would normally come from the manager.execute() method, which isn't called
|
|
||||||
// here.
|
|
||||||
return delegate.rebuild(manager, bundle);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId)
|
|
||||||
throws IOException, OrmException {
|
|
||||||
// Don't inspect stealNextUpdate; that happens in execute() below.
|
|
||||||
return delegate.stage(db, changeId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Result execute(ReviewDb db, Change.Id changeId, NoteDbUpdateManager manager)
|
|
||||||
throws OrmException, IOException {
|
|
||||||
if (failNextUpdate.getAndSet(false)) {
|
|
||||||
throw new IOException("Update failed");
|
|
||||||
}
|
|
||||||
Result result = delegate.execute(db, changeId, manager);
|
|
||||||
if (stealNextUpdate.getAndSet(false)) {
|
|
||||||
throw new IOException("Update stolen");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle)
|
|
||||||
throws IOException, OrmException {
|
|
||||||
// Don't check for manual failure; that happens in execute().
|
|
||||||
delegate.buildUpdates(manager, bundle);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void rebuildReviewDb(ReviewDb db, Project.NameKey project, Id changeId)
|
|
||||||
throws OrmException {
|
|
||||||
if (failNextUpdate.getAndSet(false)) {
|
|
||||||
throw new OrmException("Update failed");
|
|
||||||
}
|
|
||||||
delegate.rebuildReviewDb(db, project, changeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import com.google.gwtorm.server.OrmRuntimeException;
|
|
||||||
|
|
||||||
class AbortUpdateException extends OrmRuntimeException {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
AbortUpdateException() {
|
|
||||||
super("aborted");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import com.google.common.base.MoreObjects.ToStringHelper;
|
|
||||||
import com.google.gerrit.reviewdb.client.PatchSetApproval;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
|
||||||
import java.sql.Timestamp;
|
|
||||||
|
|
||||||
class ApprovalEvent extends Event {
|
|
||||||
private PatchSetApproval psa;
|
|
||||||
|
|
||||||
ApprovalEvent(PatchSetApproval psa, Timestamp changeCreatedOn) {
|
|
||||||
super(
|
|
||||||
psa.getPatchSetId(),
|
|
||||||
psa.getAccountId(),
|
|
||||||
psa.getRealAccountId(),
|
|
||||||
psa.getGranted(),
|
|
||||||
changeCreatedOn,
|
|
||||||
psa.getTag());
|
|
||||||
this.psa = psa;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
boolean uniquePerUpdate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean canHaveTag() {
|
|
||||||
// Legacy SUBM approvals don't have a tag field set, but the corresponding
|
|
||||||
// ChangeMessage for merging the change does. We need to let these be in the
|
|
||||||
// same meta commit so the SUBM approval isn't counted as post-submit.
|
|
||||||
return !psa.isLegacySubmit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void apply(ChangeUpdate update) {
|
|
||||||
checkUpdate(update);
|
|
||||||
update.putApproval(psa.getLabel(), psa.getValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isPostSubmitApproval() {
|
|
||||||
return psa.isPostSubmit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void addToString(ToStringHelper helper) {
|
|
||||||
helper.add("approval", psa);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import com.google.common.base.MoreObjects.ToStringHelper;
|
|
||||||
import com.google.common.base.Strings;
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
|
||||||
import com.google.gerrit.reviewdb.client.ChangeMessage;
|
|
||||||
import com.google.gerrit.server.ChangeMessagesUtil;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
import java.sql.Timestamp;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
class ChangeMessageEvent extends Event {
|
|
||||||
private static final ImmutableMap<Change.Status, Pattern> STATUS_PATTERNS =
|
|
||||||
ImmutableMap.of(
|
|
||||||
Change.Status.ABANDONED, Pattern.compile("^Abandoned(\n.*)*$"),
|
|
||||||
Change.Status.MERGED,
|
|
||||||
Pattern.compile(
|
|
||||||
"^Change has been successfully (merged|cherry-picked|rebased|pushed).*$"),
|
|
||||||
Change.Status.NEW, Pattern.compile("^Restored(\n.*)*$"));
|
|
||||||
|
|
||||||
private static final Pattern PRIVATE_SET_REGEXP = Pattern.compile("^Set private$");
|
|
||||||
private static final Pattern PRIVATE_UNSET_REGEXP = Pattern.compile("^Unset private$");
|
|
||||||
|
|
||||||
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 WIP_SET_REGEXP = Pattern.compile("^Set Work In Progress$");
|
|
||||||
private static final Pattern WIP_UNSET_REGEXP = Pattern.compile("^Set Ready For Review$");
|
|
||||||
|
|
||||||
private final Change change;
|
|
||||||
private final Change noteDbChange;
|
|
||||||
private final Optional<Change.Status> status;
|
|
||||||
private final ChangeMessage message;
|
|
||||||
|
|
||||||
ChangeMessageEvent(
|
|
||||||
Change change, Change noteDbChange, ChangeMessage message, Timestamp changeCreatedOn) {
|
|
||||||
super(
|
|
||||||
message.getPatchSetId(),
|
|
||||||
message.getAuthor(),
|
|
||||||
message.getRealAuthor(),
|
|
||||||
message.getWrittenOn(),
|
|
||||||
changeCreatedOn,
|
|
||||||
message.getTag());
|
|
||||||
this.change = change;
|
|
||||||
this.noteDbChange = noteDbChange;
|
|
||||||
this.message = message;
|
|
||||||
this.status = parseStatus(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
boolean uniquePerUpdate() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isSubmit() {
|
|
||||||
return status.isPresent() && status.get() == Change.Status.MERGED;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean canHaveTag() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@Override
|
|
||||||
void apply(ChangeUpdate update) throws OrmException {
|
|
||||||
checkUpdate(update);
|
|
||||||
update.setChangeMessage(message.getMessage());
|
|
||||||
setPrivate(update);
|
|
||||||
setTopic(update);
|
|
||||||
setWorkInProgress(update);
|
|
||||||
|
|
||||||
if (status.isPresent()) {
|
|
||||||
Change.Status s = status.get();
|
|
||||||
update.fixStatus(s);
|
|
||||||
noteDbChange.setStatus(s);
|
|
||||||
if (s == Change.Status.MERGED) {
|
|
||||||
update.setSubmissionId(change.getSubmissionId());
|
|
||||||
noteDbChange.setSubmissionId(change.getSubmissionId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Optional<Change.Status> parseStatus(ChangeMessage message) {
|
|
||||||
String msg = message.getMessage();
|
|
||||||
if (msg == null) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
for (Map.Entry<Change.Status, Pattern> e : STATUS_PATTERNS.entrySet()) {
|
|
||||||
if (e.getValue().matcher(msg).matches()) {
|
|
||||||
return Optional.of(e.getKey());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setPrivate(ChangeUpdate update) {
|
|
||||||
String msg = message.getMessage();
|
|
||||||
if (msg == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Matcher m = PRIVATE_SET_REGEXP.matcher(msg);
|
|
||||||
if (m.matches()) {
|
|
||||||
update.setPrivate(true);
|
|
||||||
noteDbChange.setPrivate(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m = PRIVATE_UNSET_REGEXP.matcher(msg);
|
|
||||||
if (m.matches()) {
|
|
||||||
update.setPrivate(false);
|
|
||||||
noteDbChange.setPrivate(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 setWorkInProgress(ChangeUpdate update) {
|
|
||||||
String msg = Strings.nullToEmpty(message.getMessage());
|
|
||||||
String tag = message.getTag();
|
|
||||||
if (ChangeMessagesUtil.TAG_SET_WIP.equals(tag)
|
|
||||||
|| ChangeMessagesUtil.TAG_UPLOADED_WIP_PATCH_SET.equals(tag)
|
|
||||||
|| WIP_SET_REGEXP.matcher(msg).matches()) {
|
|
||||||
update.setWorkInProgress(true);
|
|
||||||
noteDbChange.setWorkInProgress(true);
|
|
||||||
} else if (ChangeMessagesUtil.TAG_SET_READY.equals(tag)
|
|
||||||
|| ChangeMessagesUtil.TAG_UPLOADED_PATCH_SET.equals(tag)
|
|
||||||
|| WIP_UNSET_REGEXP.matcher(msg).matches()) {
|
|
||||||
update.setWorkInProgress(false);
|
|
||||||
noteDbChange.setWorkInProgress(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void addToString(ToStringHelper helper) {
|
|
||||||
helper.add("message", message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
|
||||||
import com.google.common.util.concurrent.ListeningExecutorService;
|
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
|
||||||
import com.google.gerrit.reviewdb.client.Project;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeBundle;
|
|
||||||
import com.google.gerrit.server.notedb.NoteDbUpdateManager;
|
|
||||||
import com.google.gerrit.server.notedb.NoteDbUpdateManager.Result;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
import com.google.gwtorm.server.SchemaFactory;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public abstract class ChangeRebuilder {
|
|
||||||
public static class NoPatchSetsException extends OrmException {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
NoPatchSetsException(Change.Id changeId) {
|
|
||||||
super("Change " + changeId + " cannot be rebuilt because it has no patch sets");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final SchemaFactory<ReviewDb> schemaFactory;
|
|
||||||
|
|
||||||
protected ChangeRebuilder(SchemaFactory<ReviewDb> schemaFactory) {
|
|
||||||
this.schemaFactory = schemaFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final ListenableFuture<Result> rebuildAsync(
|
|
||||||
Change.Id id, ListeningExecutorService executor) {
|
|
||||||
return executor.submit(
|
|
||||||
() -> {
|
|
||||||
try (ReviewDb db = schemaFactory.open()) {
|
|
||||||
return rebuild(db, id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rebuild ReviewDb contents by copying from NoteDb.
|
|
||||||
*
|
|
||||||
* <p>Requires NoteDb to be the primary storage for the change.
|
|
||||||
*/
|
|
||||||
public abstract void rebuildReviewDb(ReviewDb db, Project.NameKey project, Change.Id changeId)
|
|
||||||
throws OrmException;
|
|
||||||
|
|
||||||
// In the following methods "rebuilding" always refers to copying the state
|
|
||||||
// from ReviewDb to NoteDb, i.e. assuming ReviewDb is the primary storage.
|
|
||||||
|
|
||||||
public abstract Result rebuild(ReviewDb db, Change.Id changeId) throws IOException, OrmException;
|
|
||||||
|
|
||||||
public abstract Result rebuildEvenIfReadOnly(ReviewDb db, Change.Id changeId)
|
|
||||||
throws IOException, OrmException;
|
|
||||||
|
|
||||||
public abstract Result rebuild(NoteDbUpdateManager manager, ChangeBundle bundle)
|
|
||||||
throws IOException, OrmException;
|
|
||||||
|
|
||||||
public abstract void buildUpdates(NoteDbUpdateManager manager, ChangeBundle bundle)
|
|
||||||
throws IOException, OrmException;
|
|
||||||
|
|
||||||
public abstract NoteDbUpdateManager stage(ReviewDb db, Change.Id changeId)
|
|
||||||
throws IOException, OrmException;
|
|
||||||
|
|
||||||
public abstract Result execute(ReviewDb db, Change.Id changeId, NoteDbUpdateManager manager)
|
|
||||||
throws OrmException, IOException;
|
|
||||||
}
|
|
||||||
@@ -1,687 +0,0 @@
|
|||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
|
|
||||||
|
|
||||||
import com.google.common.base.MoreObjects.ToStringHelper;
|
|
||||||
import com.google.common.flogger.FluentLogger;
|
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
|
||||||
import com.google.gerrit.reviewdb.client.Comment;
|
|
||||||
import com.google.gerrit.reviewdb.client.PatchLineComment;
|
|
||||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
|
||||||
import com.google.gerrit.server.CommentsUtil;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
|
||||||
import com.google.gerrit.server.patch.PatchListCache;
|
|
||||||
import com.google.gerrit.server.patch.PatchListNotAvailableException;
|
|
||||||
|
|
||||||
class CommentEvent extends Event {
|
|
||||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
|
||||||
|
|
||||||
public final Comment c;
|
|
||||||
private final Change change;
|
|
||||||
private final PatchSet ps;
|
|
||||||
private final PatchListCache cache;
|
|
||||||
|
|
||||||
CommentEvent(Comment c, Change change, PatchSet ps, PatchListCache cache) {
|
|
||||||
super(
|
|
||||||
CommentsUtil.getCommentPsId(change.getId(), c),
|
|
||||||
c.author.getId(),
|
|
||||||
c.getRealAuthor().getId(),
|
|
||||||
c.writtenOn,
|
|
||||||
change.getCreatedOn(),
|
|
||||||
c.tag);
|
|
||||||
this.c = c;
|
|
||||||
this.change = change;
|
|
||||||
this.ps = ps;
|
|
||||||
this.cache = cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
boolean uniquePerUpdate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean canHaveTag() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void apply(ChangeUpdate update) {
|
|
||||||
checkUpdate(update);
|
|
||||||
if (c.revId == null) {
|
|
||||||
try {
|
|
||||||
setCommentRevId(c, cache, change, ps);
|
|
||||||
} catch (PatchListNotAvailableException e) {
|
|
||||||
logger.atWarning().log(
|
|
||||||
"Unable to determine parent commit of patch set %s (%s); omitting inline comment %s",
|
|
||||||
ps.getId(), ps.getRevision(), c);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
update.putComment(PatchLineComment.Status.PUBLISHED, c);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void addToString(ToStringHelper helper) {
|
|
||||||
helper.add("message", c.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
// Copyright (C) 2017 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 com.google.gwtorm.server.OrmException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link com.google.gwtorm.server.OrmException} thrown by {@link ChangeRebuilder} when rebuilding a
|
|
||||||
* change failed because another operation modified its {@link
|
|
||||||
* com.google.gerrit.server.notedb.NoteDbChangeState}.
|
|
||||||
*/
|
|
||||||
public class ConflictingUpdateException extends OrmException {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
// Always created from a ConflictingUpdateRuntimeException because it originates from an
|
|
||||||
// AtomicUpdate, which cannot throw checked exceptions.
|
|
||||||
ConflictingUpdateException(ConflictingUpdateRuntimeException cause) {
|
|
||||||
super(cause.getMessage(), cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
|
||||||
import com.google.gwtorm.server.OrmRuntimeException;
|
|
||||||
|
|
||||||
class ConflictingUpdateRuntimeException extends OrmRuntimeException {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
ConflictingUpdateRuntimeException(Change change, String expectedNoteDbState) {
|
|
||||||
super(
|
|
||||||
String.format(
|
|
||||||
"Expected change %s to have noteDbState %s but was %s",
|
|
||||||
change.getId(), expectedNoteDbState, change.getNoteDbState()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
|
||||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
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.getOwner(),
|
|
||||||
change.getCreatedOn(),
|
|
||||||
change.getCreatedOn(),
|
|
||||||
null);
|
|
||||||
this.change = change;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
boolean uniquePerUpdate() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void apply(ChangeUpdate update) throws IOException, OrmException {
|
|
||||||
checkUpdate(update);
|
|
||||||
ChangeRebuilderImpl.createChange(update, change);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import static com.google.gerrit.server.CommentsUtil.setCommentRevId;
|
|
||||||
|
|
||||||
import com.google.common.base.MoreObjects.ToStringHelper;
|
|
||||||
import com.google.common.flogger.FluentLogger;
|
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
|
||||||
import com.google.gerrit.reviewdb.client.Comment;
|
|
||||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
|
||||||
import com.google.gerrit.server.CommentsUtil;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeDraftUpdate;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
|
||||||
import com.google.gerrit.server.patch.PatchListCache;
|
|
||||||
import com.google.gerrit.server.patch.PatchListNotAvailableException;
|
|
||||||
|
|
||||||
class DraftCommentEvent extends Event {
|
|
||||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
|
||||||
|
|
||||||
public final Comment c;
|
|
||||||
private final Change change;
|
|
||||||
private final PatchSet ps;
|
|
||||||
private final PatchListCache cache;
|
|
||||||
|
|
||||||
DraftCommentEvent(Comment c, Change change, PatchSet ps, PatchListCache cache) {
|
|
||||||
super(
|
|
||||||
CommentsUtil.getCommentPsId(change.getId(), c),
|
|
||||||
c.author.getId(),
|
|
||||||
c.getRealAuthor().getId(),
|
|
||||||
c.writtenOn,
|
|
||||||
change.getCreatedOn(),
|
|
||||||
c.tag);
|
|
||||||
this.c = c;
|
|
||||||
this.change = change;
|
|
||||||
this.ps = ps;
|
|
||||||
this.cache = cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
boolean uniquePerUpdate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void apply(ChangeUpdate update) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
void applyDraft(ChangeDraftUpdate draftUpdate) {
|
|
||||||
if (c.revId == null) {
|
|
||||||
try {
|
|
||||||
setCommentRevId(c, cache, change, ps);
|
|
||||||
} catch (PatchListNotAvailableException e) {
|
|
||||||
logger.atWarning().log(
|
|
||||||
"Unable to determine parent commit of patch set %s (%s);"
|
|
||||||
+ " omitting draft inline comment %s",
|
|
||||||
ps.getId(), ps.getRevision(), c);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
draftUpdate.putComment(c);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void addToString(ToStringHelper helper) {
|
|
||||||
helper.add("message", c.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkState;
|
|
||||||
import static com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl.MAX_WINDOW_MS;
|
|
||||||
|
|
||||||
import com.google.common.base.MoreObjects;
|
|
||||||
import com.google.common.base.MoreObjects.ToStringHelper;
|
|
||||||
import com.google.common.collect.ComparisonChain;
|
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
|
||||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDbUtil;
|
|
||||||
import com.google.gerrit.server.notedb.AbstractChangeUpdate;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.sql.Timestamp;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
abstract class Event implements Comparable<Event> {
|
|
||||||
// NOTE: EventList only supports direct subclasses, not an arbitrary
|
|
||||||
// hierarchy.
|
|
||||||
|
|
||||||
final Account.Id user;
|
|
||||||
final Account.Id realUser;
|
|
||||||
final String tag;
|
|
||||||
final boolean predatesChange;
|
|
||||||
|
|
||||||
/** Dependencies of this event; other events that must happen before this one. */
|
|
||||||
final List<Event> deps;
|
|
||||||
|
|
||||||
Timestamp when;
|
|
||||||
PatchSet.Id psId;
|
|
||||||
|
|
||||||
protected Event(
|
|
||||||
PatchSet.Id psId,
|
|
||||||
Account.Id effectiveUser,
|
|
||||||
Account.Id realUser,
|
|
||||||
Timestamp when,
|
|
||||||
Timestamp changeCreatedOn,
|
|
||||||
String tag) {
|
|
||||||
this.psId = psId;
|
|
||||||
this.user = effectiveUser;
|
|
||||||
this.realUser = realUser != null ? realUser : effectiveUser;
|
|
||||||
this.tag = tag;
|
|
||||||
// Truncate timestamps at the change's createdOn timestamp.
|
|
||||||
predatesChange = when.before(changeCreatedOn);
|
|
||||||
this.when = predatesChange ? changeCreatedOn : when;
|
|
||||||
deps = new ArrayList<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
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(), user),
|
|
||||||
"cannot apply event by %s to update by %s",
|
|
||||||
user,
|
|
||||||
update.getNullableAccountId());
|
|
||||||
}
|
|
||||||
|
|
||||||
Event addDep(Event e) {
|
|
||||||
deps.add(e);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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;
|
|
||||||
|
|
||||||
protected boolean isPostSubmitApproval() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean isSubmit() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean canHaveTag() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
ToStringHelper helper =
|
|
||||||
MoreObjects.toStringHelper(this)
|
|
||||||
.add("psId", psId)
|
|
||||||
.add("effectiveUser", user)
|
|
||||||
.add("realUser", realUser)
|
|
||||||
.add("when", when)
|
|
||||||
.add("tag", tag);
|
|
||||||
addToString(helper);
|
|
||||||
return helper.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param helper toString helper to add fields to */
|
|
||||||
protected void addToString(ToStringHelper helper) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(Event other) {
|
|
||||||
return ComparisonChain.start()
|
|
||||||
.compareFalseFirst(this.isFinalUpdates(), other.isFinalUpdates())
|
|
||||||
.compare(this.when, other.when)
|
|
||||||
.compareTrueFirst(isPatchSet(), isPatchSet())
|
|
||||||
.compareTrueFirst(this.predatesChange, other.predatesChange)
|
|
||||||
.compare(this.user, other.user, ReviewDbUtil.intKeyOrdering())
|
|
||||||
.compare(this.realUser, other.realUser, ReviewDbUtil.intKeyOrdering())
|
|
||||||
.compare(this.psId, other.psId, ReviewDbUtil.intKeyOrdering().nullsLast())
|
|
||||||
.result();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isPatchSet() {
|
|
||||||
return this instanceof PatchSetEvent;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isFinalUpdates() {
|
|
||||||
return this instanceof FinalUpdatesEvent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkArgument;
|
|
||||||
import static com.google.common.base.Preconditions.checkState;
|
|
||||||
import static java.util.Objects.requireNonNull;
|
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
|
||||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
|
||||||
import java.sql.Timestamp;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
class EventList<E extends Event> implements Iterable<E> {
|
|
||||||
private final ArrayList<E> list = new ArrayList<>();
|
|
||||||
private boolean isSubmit;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterator<E> iterator() {
|
|
||||||
return list.iterator();
|
|
||||||
}
|
|
||||||
|
|
||||||
void add(E e) {
|
|
||||||
list.add(e);
|
|
||||||
if (e.isSubmit()) {
|
|
||||||
isSubmit = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void clear() {
|
|
||||||
list.clear();
|
|
||||||
isSubmit = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isEmpty() {
|
|
||||||
return list.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
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.user, last.user)
|
|
||||||
|| !Objects.equals(e.realUser, last.realUser)
|
|
||||||
|| !e.psId.equals(last.psId)) {
|
|
||||||
return false; // Different patch set or author.
|
|
||||||
}
|
|
||||||
if (e.canHaveTag() && canHaveTag() && !Objects.equals(e.tag, getTag())) {
|
|
||||||
// We should trust the tag field, and it doesn't match.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (e.isPostSubmitApproval() && isSubmit) {
|
|
||||||
// Post-submit approvals must come after the update that submits.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 > ChangeRebuilderImpl.MAX_DELTA_MS
|
|
||||||
|| t - tFirst > ChangeRebuilderImpl.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 = requireNonNull(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).user;
|
|
||||||
for (int i = 1; i < size(); i++) {
|
|
||||||
checkState(
|
|
||||||
Objects.equals(id, get(i).user),
|
|
||||||
"mismatched users in EventList: %s != %s",
|
|
||||||
id,
|
|
||||||
get(i).user);
|
|
||||||
}
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
Account.Id getRealAccountId() {
|
|
||||||
Account.Id id = get(0).realUser;
|
|
||||||
for (int i = 1; i < size(); i++) {
|
|
||||||
checkState(
|
|
||||||
Objects.equals(id, get(i).realUser),
|
|
||||||
"mismatched real users in EventList: %s != %s",
|
|
||||||
id,
|
|
||||||
get(i).realUser);
|
|
||||||
}
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
String getTag() {
|
|
||||||
for (E e : Lists.reverse(list)) {
|
|
||||||
if (e.tag != null) {
|
|
||||||
return e.tag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean canHaveTag() {
|
|
||||||
return list.stream().anyMatch(Event::canHaveTag);
|
|
||||||
}
|
|
||||||
|
|
||||||
private E get(int i) {
|
|
||||||
return list.get(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int size() {
|
|
||||||
return list.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
private E getLast() {
|
|
||||||
return list.get(list.size() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private long getLastTime() {
|
|
||||||
return getLast().when.getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
private long getFirstTime() {
|
|
||||||
return list.get(0).when.getTime();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import static com.google.common.base.Preconditions.checkArgument;
|
|
||||||
import static com.google.common.base.Preconditions.checkState;
|
|
||||||
|
|
||||||
import com.google.common.collect.ListMultimap;
|
|
||||||
import com.google.common.collect.MultimapBuilder;
|
|
||||||
import com.google.common.collect.SetMultimap;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.PriorityQueue;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to sort a list of events.
|
|
||||||
*
|
|
||||||
* <p>Events are sorted in two passes:
|
|
||||||
*
|
|
||||||
* <ol>
|
|
||||||
* <li>Sort by natural order (timestamp, patch set, author, etc.)
|
|
||||||
* <li>Postpone any events with dependencies to occur only after all of their dependencies, where
|
|
||||||
* this violates natural order.
|
|
||||||
* </ol>
|
|
||||||
*
|
|
||||||
* {@link #sort()} modifies the event list in place (similar to {@link Collections#sort(List)}), but
|
|
||||||
* does not modify any event. In particular, events might end up out of order with respect to
|
|
||||||
* timestamp; callers are responsible for adjusting timestamps later if they prefer monotonicity.
|
|
||||||
*/
|
|
||||||
class EventSorter {
|
|
||||||
private final List<Event> out;
|
|
||||||
private final LinkedHashSet<Event> sorted;
|
|
||||||
private ListMultimap<Event, Event> waiting;
|
|
||||||
private SetMultimap<Event, Event> deps;
|
|
||||||
|
|
||||||
EventSorter(List<Event> events) {
|
|
||||||
LinkedHashSet<Event> all = new LinkedHashSet<>(events);
|
|
||||||
out = events;
|
|
||||||
|
|
||||||
for (Event e : events) {
|
|
||||||
for (Event d : e.deps) {
|
|
||||||
checkArgument(all.contains(d), "dep %s of %s not in input list", d, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
all.clear();
|
|
||||||
sorted = all; // Presized.
|
|
||||||
}
|
|
||||||
|
|
||||||
void sort() {
|
|
||||||
// First pass: sort by natural order.
|
|
||||||
PriorityQueue<Event> todo = new PriorityQueue<>(out);
|
|
||||||
|
|
||||||
// Populate waiting map after initial sort to preserve natural order.
|
|
||||||
waiting = MultimapBuilder.hashKeys().arrayListValues().build();
|
|
||||||
deps = MultimapBuilder.hashKeys().hashSetValues().build();
|
|
||||||
for (Event e : todo) {
|
|
||||||
for (Event d : e.deps) {
|
|
||||||
deps.put(e, d);
|
|
||||||
waiting.put(d, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Second pass: enforce dependencies.
|
|
||||||
int size = out.size();
|
|
||||||
while (!todo.isEmpty()) {
|
|
||||||
process(todo.remove(), todo);
|
|
||||||
}
|
|
||||||
checkState(
|
|
||||||
sorted.size() == size, "event sort expected %s elements, got %s", size, sorted.size());
|
|
||||||
|
|
||||||
// Modify out in-place a la Collections#sort.
|
|
||||||
out.clear();
|
|
||||||
out.addAll(sorted);
|
|
||||||
}
|
|
||||||
|
|
||||||
void process(Event e, PriorityQueue<Event> todo) {
|
|
||||||
if (sorted.contains(e)) {
|
|
||||||
return; // Already emitted.
|
|
||||||
}
|
|
||||||
if (!deps.get(e).isEmpty()) {
|
|
||||||
// Not all events that e depends on have been emitted yet. Ignore e for
|
|
||||||
// now; it will get added back to the queue in the block below once its
|
|
||||||
// last dependency is processed.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// All events that e depends on have been emitted, so e can be emitted.
|
|
||||||
sorted.add(e);
|
|
||||||
|
|
||||||
// Remove e from the dependency set of all events waiting on e, and add
|
|
||||||
// those events back to the queue in the original priority order for
|
|
||||||
// reconsideration.
|
|
||||||
for (Event w : waiting.get(e)) {
|
|
||||||
deps.get(w).remove(e);
|
|
||||||
todo.add(w);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import static com.google.gerrit.reviewdb.server.ReviewDbUtil.intKeyOrdering;
|
|
||||||
|
|
||||||
import com.google.common.base.MoreObjects.ToStringHelper;
|
|
||||||
import com.google.common.collect.ImmutableCollection;
|
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
|
||||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
class FinalUpdatesEvent extends Event {
|
|
||||||
private final Change change;
|
|
||||||
private final Change noteDbChange;
|
|
||||||
private final ImmutableCollection<PatchSet> patchSets;
|
|
||||||
|
|
||||||
FinalUpdatesEvent(Change change, Change noteDbChange, ImmutableCollection<PatchSet> patchSets) {
|
|
||||||
super(
|
|
||||||
change.currentPatchSetId(),
|
|
||||||
change.getOwner(),
|
|
||||||
change.getOwner(),
|
|
||||||
change.getLastUpdatedOn(),
|
|
||||||
change.getCreatedOn(),
|
|
||||||
null);
|
|
||||||
this.change = change;
|
|
||||||
this.noteDbChange = noteDbChange;
|
|
||||||
this.patchSets = patchSets;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 (!statusMatches()) {
|
|
||||||
// TODO(dborowitz): Stamp approximate approvals at this time.
|
|
||||||
update.fixStatus(change.getStatus());
|
|
||||||
}
|
|
||||||
if (change.isPrivate() != noteDbChange.isPrivate()) {
|
|
||||||
update.setPrivate(change.isPrivate());
|
|
||||||
}
|
|
||||||
if (change.isWorkInProgress() != noteDbChange.isWorkInProgress()) {
|
|
||||||
update.setWorkInProgress(change.isWorkInProgress());
|
|
||||||
}
|
|
||||||
if (change.getSubmissionId() != null && noteDbChange.getSubmissionId() == null) {
|
|
||||||
update.setSubmissionId(change.getSubmissionId());
|
|
||||||
}
|
|
||||||
if (!Objects.equals(change.getAssignee(), noteDbChange.getAssignee())) {
|
|
||||||
// TODO(dborowitz): Parse intermediate values out from messages.
|
|
||||||
update.setAssignee(change.getAssignee());
|
|
||||||
}
|
|
||||||
if (!patchSets.isEmpty() && !highestNumberedPatchSetIsCurrent()) {
|
|
||||||
update.setCurrentPatchSet();
|
|
||||||
}
|
|
||||||
if (!update.isEmpty()) {
|
|
||||||
update.setSubjectForCommit("Final NoteDb migration updates");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean statusMatches() {
|
|
||||||
return Objects.equals(change.getStatus(), noteDbChange.getStatus());
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean highestNumberedPatchSetIsCurrent() {
|
|
||||||
PatchSet.Id max = patchSets.stream().map(PatchSet::getId).max(intKeyOrdering()).get();
|
|
||||||
return max.equals(change.currentPatchSetId());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean isSubmit() {
|
|
||||||
return change.getStatus() == Change.Status.MERGED;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void addToString(ToStringHelper helper) {
|
|
||||||
if (!statusMatches()) {
|
|
||||||
helper.add("status", change.getStatus());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
// Copyright (C) 2018 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 java.util.Objects.requireNonNull;
|
|
||||||
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_GC_SECTION;
|
|
||||||
import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_AUTO;
|
|
||||||
|
|
||||||
import com.google.common.base.Throwables;
|
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import com.google.common.flogger.FluentLogger;
|
|
||||||
import com.google.gerrit.common.Nullable;
|
|
||||||
import com.google.gerrit.common.data.GarbageCollectionResult;
|
|
||||||
import com.google.gerrit.server.config.AllUsersName;
|
|
||||||
import com.google.gerrit.server.git.GarbageCollection;
|
|
||||||
import com.google.gerrit.server.git.GitRepositoryManager;
|
|
||||||
import com.google.gerrit.server.git.LocalDiskRepositoryManager;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import com.google.inject.Singleton;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.util.function.Consumer;
|
|
||||||
import org.eclipse.jgit.lib.Repository;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class GcAllUsers {
|
|
||||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
|
||||||
|
|
||||||
private final AllUsersName allUsers;
|
|
||||||
private final GarbageCollection.Factory gcFactory;
|
|
||||||
private final GitRepositoryManager repoManager;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
GcAllUsers(
|
|
||||||
AllUsersName allUsers,
|
|
||||||
GarbageCollection.Factory gcFactory,
|
|
||||||
GitRepositoryManager repoManager) {
|
|
||||||
this.allUsers = allUsers;
|
|
||||||
this.gcFactory = gcFactory;
|
|
||||||
this.repoManager = repoManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void runWithLogger() {
|
|
||||||
// Print log messages using logger, and skip progress.
|
|
||||||
run(s -> logger.atInfo().log(s), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void run(PrintWriter writer) {
|
|
||||||
// Print both log messages and progress to given writer.
|
|
||||||
run(requireNonNull(writer)::println, writer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void run(Consumer<String> logOneLine, @Nullable PrintWriter progressWriter) {
|
|
||||||
if (!(repoManager instanceof LocalDiskRepositoryManager)) {
|
|
||||||
logOneLine.accept("Skipping GC of " + allUsers + "; not a local disk repo");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!enableAutoGc(logOneLine)) {
|
|
||||||
logOneLine.accept(
|
|
||||||
"Skipping GC of "
|
|
||||||
+ allUsers
|
|
||||||
+ " due to disabling "
|
|
||||||
+ CONFIG_GC_SECTION
|
|
||||||
+ "."
|
|
||||||
+ CONFIG_KEY_AUTO);
|
|
||||||
logOneLine.accept(
|
|
||||||
"If loading accounts is slow after the NoteDb migration, run `git gc` on "
|
|
||||||
+ allUsers
|
|
||||||
+ " manually");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progressWriter == null) {
|
|
||||||
// Mimic log line from GarbageCollection.
|
|
||||||
logOneLine.accept("collecting garbage for \"" + allUsers + "\":\n");
|
|
||||||
}
|
|
||||||
GarbageCollectionResult result =
|
|
||||||
gcFactory.create().run(ImmutableList.of(allUsers), progressWriter);
|
|
||||||
if (!result.hasErrors()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (GarbageCollectionResult.Error e : result.getErrors()) {
|
|
||||||
switch (e.getType()) {
|
|
||||||
case GC_ALREADY_SCHEDULED:
|
|
||||||
logOneLine.accept("GC already scheduled for " + e.getProjectName());
|
|
||||||
break;
|
|
||||||
case GC_FAILED:
|
|
||||||
logOneLine.accept("GC failed for " + e.getProjectName());
|
|
||||||
break;
|
|
||||||
case REPOSITORY_NOT_FOUND:
|
|
||||||
logOneLine.accept(e.getProjectName() + " repo not found");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
logOneLine.accept("GC failed for " + e.getProjectName() + ": " + e.getType());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean enableAutoGc(Consumer<String> logOneLine) {
|
|
||||||
try (Repository repo = repoManager.openRepository(allUsers)) {
|
|
||||||
return repo.getConfig().getInt(CONFIG_GC_SECTION, CONFIG_KEY_AUTO, -1) != 0;
|
|
||||||
} catch (IOException e) {
|
|
||||||
logOneLine.accept(
|
|
||||||
"Error reading config for " + allUsers + ":\n" + Throwables.getStackTraceAsString(e));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import com.google.common.base.MoreObjects.ToStringHelper;
|
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
|
||||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
import java.sql.Timestamp;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
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,
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void addToString(ToStringHelper helper) {
|
|
||||||
helper.add("hashtags", hashtags);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
|
||||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
import org.eclipse.jgit.errors.InvalidObjectIdException;
|
|
||||||
import org.eclipse.jgit.errors.MissingObjectException;
|
|
||||||
import org.eclipse.jgit.lib.ObjectId;
|
|
||||||
import org.eclipse.jgit.revwalk.RevWalk;
|
|
||||||
|
|
||||||
class PatchSetEvent extends Event {
|
|
||||||
private final Change change;
|
|
||||||
private final PatchSet ps;
|
|
||||||
private final RevWalk rw;
|
|
||||||
boolean createChange;
|
|
||||||
|
|
||||||
PatchSetEvent(Change change, PatchSet ps, RevWalk rw) {
|
|
||||||
super(
|
|
||||||
ps.getId(),
|
|
||||||
ps.getUploader(),
|
|
||||||
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) {
|
|
||||||
ChangeRebuilderImpl.createChange(update, change);
|
|
||||||
} else {
|
|
||||||
update.setSubject(change.getSubject());
|
|
||||||
update.setSubjectForCommit("Create patch set " + ps.getPatchSetId());
|
|
||||||
}
|
|
||||||
setRevision(update, ps);
|
|
||||||
update.setPsDescription(ps.getDescription());
|
|
||||||
List<String> groups = ps.getGroups();
|
|
||||||
if (!groups.isEmpty()) {
|
|
||||||
update.setGroups(ps.getGroups());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import com.google.common.base.MoreObjects.ToStringHelper;
|
|
||||||
import com.google.common.collect.Table;
|
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
|
||||||
import com.google.gerrit.server.notedb.ReviewerStateInternal;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.sql.Timestamp;
|
|
||||||
|
|
||||||
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(),
|
|
||||||
// TODO(dborowitz): Real account ID shouldn't really matter for
|
|
||||||
// reviewers, but we might have to deal with this to avoid ChangeBundle
|
|
||||||
// diffs when run against real data.
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void addToString(ToStringHelper helper) {
|
|
||||||
helper.add("account", reviewer.getColumnKey()).add("state", reviewer.getRowKey());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,8 +16,6 @@ package com.google.gerrit.server.schema;
|
|||||||
|
|
||||||
import com.google.gerrit.extensions.config.FactoryModule;
|
import com.google.gerrit.extensions.config.FactoryModule;
|
||||||
import com.google.gerrit.reviewdb.server.ReviewDb;
|
import com.google.gerrit.reviewdb.server.ReviewDb;
|
||||||
import com.google.gerrit.server.notedb.ChangeBundleReader;
|
|
||||||
import com.google.gerrit.server.notedb.GwtormChangeBundleReader;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
import com.google.gwtorm.server.OrmException;
|
||||||
import com.google.gwtorm.server.SchemaFactory;
|
import com.google.gwtorm.server.SchemaFactory;
|
||||||
import com.google.inject.Key;
|
import com.google.inject.Key;
|
||||||
@@ -35,6 +33,5 @@ public class DatabaseModule extends FactoryModule {
|
|||||||
() -> {
|
() -> {
|
||||||
throw new OrmException("ReviewDb no longer exists");
|
throw new OrmException("ReviewDb no longer exists");
|
||||||
});
|
});
|
||||||
bind(ChangeBundleReader.class).to(GwtormChangeBundleReader.class);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,8 +73,6 @@ import com.google.gerrit.server.index.group.AllGroupsIndexer;
|
|||||||
import com.google.gerrit.server.index.group.GroupIndexCollection;
|
import com.google.gerrit.server.index.group.GroupIndexCollection;
|
||||||
import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
|
import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
|
||||||
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
|
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
|
||||||
import com.google.gerrit.server.notedb.ChangeBundleReader;
|
|
||||||
import com.google.gerrit.server.notedb.GwtormChangeBundleReader;
|
|
||||||
import com.google.gerrit.server.notedb.MutableNotesMigration;
|
import com.google.gerrit.server.notedb.MutableNotesMigration;
|
||||||
import com.google.gerrit.server.notedb.NotesMigration;
|
import com.google.gerrit.server.notedb.NotesMigration;
|
||||||
import com.google.gerrit.server.patch.DiffExecutor;
|
import com.google.gerrit.server.patch.DiffExecutor;
|
||||||
@@ -198,7 +196,6 @@ public class InMemoryModule extends FactoryModule {
|
|||||||
bind(ListeningExecutorService.class)
|
bind(ListeningExecutorService.class)
|
||||||
.annotatedWith(ChangeUpdateExecutor.class)
|
.annotatedWith(ChangeUpdateExecutor.class)
|
||||||
.toInstance(MoreExecutors.newDirectExecutorService());
|
.toInstance(MoreExecutors.newDirectExecutorService());
|
||||||
bind(ChangeBundleReader.class).to(GwtormChangeBundleReader.class);
|
|
||||||
bind(SecureStore.class).to(DefaultSecureStore.class);
|
bind(SecureStore.class).to(DefaultSecureStore.class);
|
||||||
|
|
||||||
TypeLiteral<SchemaFactory<ReviewDb>> schemaFactory =
|
TypeLiteral<SchemaFactory<ReviewDb>> schemaFactory =
|
||||||
|
|||||||
@@ -1,225 +0,0 @@
|
|||||||
// 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.testing;
|
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
|
||||||
import static java.util.Comparator.comparing;
|
|
||||||
import static java.util.stream.Collectors.toList;
|
|
||||||
|
|
||||||
import com.google.common.base.Joiner;
|
|
||||||
import com.google.common.collect.ImmutableListMultimap;
|
|
||||||
import com.google.common.collect.ListMultimap;
|
|
||||||
import com.google.common.flogger.FluentLogger;
|
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
|
||||||
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.git.GitRepositoryManager;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeBundle;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeBundleReader;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeNotes;
|
|
||||||
import com.google.gerrit.server.notedb.MutableNotesMigration;
|
|
||||||
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilder;
|
|
||||||
import com.google.gwtorm.client.IntKey;
|
|
||||||
import com.google.gwtorm.server.OrmException;
|
|
||||||
import com.google.gwtorm.server.OrmRuntimeException;
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import com.google.inject.Provider;
|
|
||||||
import com.google.inject.Singleton;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
|
||||||
import org.eclipse.jgit.lib.Repository;
|
|
||||||
import org.junit.runner.Description;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class NoteDbChecker {
|
|
||||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
|
||||||
|
|
||||||
private final Provider<ReviewDb> dbProvider;
|
|
||||||
private final GitRepositoryManager repoManager;
|
|
||||||
private final MutableNotesMigration notesMigration;
|
|
||||||
private final ChangeBundleReader bundleReader;
|
|
||||||
private final ChangeNotes.Factory notesFactory;
|
|
||||||
private final ChangeRebuilder changeRebuilder;
|
|
||||||
private final CommentsUtil commentsUtil;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
NoteDbChecker(
|
|
||||||
Provider<ReviewDb> dbProvider,
|
|
||||||
GitRepositoryManager repoManager,
|
|
||||||
MutableNotesMigration notesMigration,
|
|
||||||
ChangeBundleReader bundleReader,
|
|
||||||
ChangeNotes.Factory notesFactory,
|
|
||||||
ChangeRebuilder changeRebuilder,
|
|
||||||
CommentsUtil commentsUtil) {
|
|
||||||
this.dbProvider = dbProvider;
|
|
||||||
this.repoManager = repoManager;
|
|
||||||
this.bundleReader = bundleReader;
|
|
||||||
this.notesMigration = notesMigration;
|
|
||||||
this.notesFactory = notesFactory;
|
|
||||||
this.changeRebuilder = changeRebuilder;
|
|
||||||
this.commentsUtil = commentsUtil;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void rebuildAndCheckAllChanges() throws Exception {
|
|
||||||
rebuildAndCheckChanges(
|
|
||||||
getUnwrappedDb().changes().all().toList().stream().map(Change::getId),
|
|
||||||
ImmutableListMultimap.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void rebuildAndCheckChanges(Change.Id... changeIds) throws Exception {
|
|
||||||
rebuildAndCheckChanges(Arrays.stream(changeIds), ImmutableListMultimap.of());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void rebuildAndCheckChanges(
|
|
||||||
Stream<Change.Id> changeIds, ListMultimap<Change.Id, String> expectedDiffs) throws Exception {
|
|
||||||
ReviewDb db = getUnwrappedDb();
|
|
||||||
|
|
||||||
List<ChangeBundle> allExpected = readExpected(changeIds);
|
|
||||||
|
|
||||||
boolean oldWrite = notesMigration.rawWriteChangesSetting();
|
|
||||||
boolean oldRead = notesMigration.readChanges();
|
|
||||||
try {
|
|
||||||
notesMigration.setWriteChanges(true);
|
|
||||||
notesMigration.setReadChanges(true);
|
|
||||||
List<String> msgs = new ArrayList<>();
|
|
||||||
for (ChangeBundle expected : allExpected) {
|
|
||||||
Change c = expected.getChange();
|
|
||||||
try {
|
|
||||||
changeRebuilder.rebuild(db, c.getId());
|
|
||||||
} catch (RepositoryNotFoundException e) {
|
|
||||||
msgs.add("Repository not found for change, cannot convert: " + c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
checkActual(allExpected, expectedDiffs, msgs);
|
|
||||||
} finally {
|
|
||||||
notesMigration.setReadChanges(oldRead);
|
|
||||||
notesMigration.setWriteChanges(oldWrite);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkChanges(Change.Id... changeIds) throws Exception {
|
|
||||||
checkActual(
|
|
||||||
readExpected(Arrays.stream(changeIds)), ImmutableListMultimap.of(), new ArrayList<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void rebuildAndCheckChange(Change.Id changeId, String... expectedDiff) throws Exception {
|
|
||||||
ImmutableListMultimap.Builder<Change.Id, String> b = ImmutableListMultimap.builder();
|
|
||||||
b.putAll(changeId, Arrays.asList(expectedDiff));
|
|
||||||
rebuildAndCheckChanges(Stream.of(changeId), b.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void assertNoChangeRef(Project.NameKey project, Change.Id changeId) throws Exception {
|
|
||||||
try (Repository repo = repoManager.openRepository(project)) {
|
|
||||||
assertThat(repo.exactRef(RefNames.changeMetaRef(changeId))).isNull();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void assertNoReviewDbChanges(Description desc) throws Exception {
|
|
||||||
ReviewDb db = getUnwrappedDb();
|
|
||||||
assertThat(db.changes().all().toList()).named("Changes in " + desc.getTestClass()).isEmpty();
|
|
||||||
assertThat(db.changeMessages().all().toList())
|
|
||||||
.named("ChangeMessages in " + desc.getTestClass())
|
|
||||||
.isEmpty();
|
|
||||||
assertThat(db.patchSets().all().toList())
|
|
||||||
.named("PatchSets in " + desc.getTestClass())
|
|
||||||
.isEmpty();
|
|
||||||
assertThat(db.patchSetApprovals().all().toList())
|
|
||||||
.named("PatchSetApprovals in " + desc.getTestClass())
|
|
||||||
.isEmpty();
|
|
||||||
assertThat(db.patchComments().all().toList())
|
|
||||||
.named("PatchLineComments in " + desc.getTestClass())
|
|
||||||
.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<ChangeBundle> readExpected(Stream<Change.Id> changeIds) throws Exception {
|
|
||||||
boolean old = notesMigration.readChanges();
|
|
||||||
try {
|
|
||||||
notesMigration.setReadChanges(false);
|
|
||||||
return changeIds
|
|
||||||
.sorted(comparing(IntKey::get))
|
|
||||||
.map(this::readBundleUnchecked)
|
|
||||||
.collect(toList());
|
|
||||||
} finally {
|
|
||||||
notesMigration.setReadChanges(old);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ChangeBundle readBundleUnchecked(Change.Id id) {
|
|
||||||
try {
|
|
||||||
return bundleReader.fromReviewDb(getUnwrappedDb(), id);
|
|
||||||
} catch (OrmException e) {
|
|
||||||
throw new OrmRuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkActual(
|
|
||||||
List<ChangeBundle> allExpected,
|
|
||||||
ListMultimap<Change.Id, String> expectedDiffs,
|
|
||||||
List<String> msgs)
|
|
||||||
throws Exception {
|
|
||||||
ReviewDb db = getUnwrappedDb();
|
|
||||||
boolean oldRead = notesMigration.readChanges();
|
|
||||||
boolean oldWrite = notesMigration.rawWriteChangesSetting();
|
|
||||||
try {
|
|
||||||
notesMigration.setWriteChanges(true);
|
|
||||||
notesMigration.setReadChanges(true);
|
|
||||||
for (ChangeBundle expected : allExpected) {
|
|
||||||
Change c = expected.getChange();
|
|
||||||
ChangeBundle actual;
|
|
||||||
try {
|
|
||||||
actual =
|
|
||||||
ChangeBundle.fromNotes(
|
|
||||||
commentsUtil, notesFactory.create(db, c.getProject(), c.getId()));
|
|
||||||
} catch (Throwable t) {
|
|
||||||
String msg = "Error converting change: " + c;
|
|
||||||
msgs.add(msg);
|
|
||||||
logger.atSevere().withCause(t).log(msg);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
List<String> diff = expected.differencesFrom(actual);
|
|
||||||
List<String> expectedDiff = expectedDiffs.get(c.getId());
|
|
||||||
if (!diff.equals(expectedDiff)) {
|
|
||||||
msgs.add("Differences between ReviewDb and NoteDb for " + c + ":");
|
|
||||||
msgs.addAll(diff);
|
|
||||||
if (!expectedDiff.isEmpty()) {
|
|
||||||
msgs.add("Expected differences:");
|
|
||||||
msgs.addAll(expectedDiff);
|
|
||||||
}
|
|
||||||
msgs.add("");
|
|
||||||
} else {
|
|
||||||
System.err.println("NoteDb conversion of change " + c.getId() + " successful");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
notesMigration.setReadChanges(oldRead);
|
|
||||||
notesMigration.setWriteChanges(oldWrite);
|
|
||||||
}
|
|
||||||
if (!msgs.isEmpty()) {
|
|
||||||
throw new AssertionError(Joiner.on('\n').join(msgs));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ReviewDb getUnwrappedDb() {
|
|
||||||
ReviewDb db = dbProvider.get();
|
|
||||||
return ReviewDbUtil.unwrapDb(db);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -123,7 +123,6 @@ import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
|
|||||||
import com.google.gerrit.server.git.meta.MetaDataUpdate;
|
import com.google.gerrit.server.git.meta.MetaDataUpdate;
|
||||||
import com.google.gerrit.server.index.account.AccountIndexer;
|
import com.google.gerrit.server.index.account.AccountIndexer;
|
||||||
import com.google.gerrit.server.index.account.StalenessChecker;
|
import com.google.gerrit.server.index.account.StalenessChecker;
|
||||||
import com.google.gerrit.server.notedb.rebuild.ChangeRebuilderImpl;
|
|
||||||
import com.google.gerrit.server.project.ProjectConfig;
|
import com.google.gerrit.server.project.ProjectConfig;
|
||||||
import com.google.gerrit.server.project.RefPattern;
|
import com.google.gerrit.server.project.RefPattern;
|
||||||
import com.google.gerrit.server.query.account.InternalAccountQuery;
|
import com.google.gerrit.server.query.account.InternalAccountQuery;
|
||||||
@@ -468,7 +467,7 @@ public class AccountIT extends AbstractDaemonTest {
|
|||||||
RevCommit c = rw.parseCommit(ref.getObjectId());
|
RevCommit c = rw.parseCommit(ref.getObjectId());
|
||||||
long timestampDiffMs =
|
long timestampDiffMs =
|
||||||
Math.abs(c.getCommitTime() * 1000L - getAccount(accountId).getRegisteredOn().getTime());
|
Math.abs(c.getCommitTime() * 1000L - getAccount(accountId).getRegisteredOn().getTime());
|
||||||
assertThat(timestampDiffMs).isAtMost(ChangeRebuilderImpl.MAX_WINDOW_MS);
|
assertThat(timestampDiffMs).isAtMost(SECONDS.toMillis(1));
|
||||||
|
|
||||||
// Check the 'account.config' file.
|
// Check the 'account.config' file.
|
||||||
try (TreeWalk tw = TreeWalk.forPath(or, AccountProperties.ACCOUNT_CONFIG, c.getTree())) {
|
try (TreeWalk tw = TreeWalk.forPath(or, AccountProperties.ACCOUNT_CONFIG, c.getTree())) {
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ public abstract class AbstractChangeNotesTest extends GerritBaseTests {
|
|||||||
install(new GitModule());
|
install(new GitModule());
|
||||||
|
|
||||||
install(new DefaultUrlFormatter.Module());
|
install(new DefaultUrlFormatter.Module());
|
||||||
install(NoteDbModule.forTest(testConfig));
|
install(NoteDbModule.forTest());
|
||||||
bind(AllUsersName.class).toProvider(AllUsersNameProvider.class);
|
bind(AllUsersName.class).toProvider(AllUsersNameProvider.class);
|
||||||
bind(String.class).annotatedWith(GerritServerId.class).toInstance("gerrit");
|
bind(String.class).annotatedWith(GerritServerId.class).toInstance("gerrit");
|
||||||
bind(GitRepositoryManager.class).toInstance(repoManager);
|
bind(GitRepositoryManager.class).toInstance(repoManager);
|
||||||
@@ -172,11 +172,6 @@ public abstract class AbstractChangeNotesTest extends GerritBaseTests {
|
|||||||
() -> {
|
() -> {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
});
|
});
|
||||||
bind(ChangeBundleReader.class)
|
|
||||||
.toInstance(
|
|
||||||
(db, id) -> {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,232 +0,0 @@
|
|||||||
// 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.rebuild;
|
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
|
||||||
import static java.util.stream.Collectors.toList;
|
|
||||||
import static org.junit.Assert.fail;
|
|
||||||
|
|
||||||
import com.google.common.collect.Collections2;
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
import com.google.gerrit.reviewdb.client.Account;
|
|
||||||
import com.google.gerrit.reviewdb.client.Change;
|
|
||||||
import com.google.gerrit.reviewdb.client.PatchSet;
|
|
||||||
import com.google.gerrit.server.notedb.ChangeUpdate;
|
|
||||||
import com.google.gerrit.server.util.time.TimeUtil;
|
|
||||||
import com.google.gerrit.testing.GerritBaseTests;
|
|
||||||
import com.google.gerrit.testing.TestTimeUtil;
|
|
||||||
import java.sql.Timestamp;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
import org.junit.Before;
|
|
||||||
import org.junit.Test;
|
|
||||||
|
|
||||||
public class EventSorterTest extends GerritBaseTests {
|
|
||||||
private class TestEvent extends Event {
|
|
||||||
protected TestEvent(Timestamp when) {
|
|
||||||
super(
|
|
||||||
new PatchSet.Id(new Change.Id(1), 1),
|
|
||||||
new Account.Id(1000),
|
|
||||||
new Account.Id(1000),
|
|
||||||
when,
|
|
||||||
changeCreatedOn,
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
boolean uniquePerUpdate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void apply(ChangeUpdate update) {
|
|
||||||
throw new UnsupportedOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "E{" + when.getSeconds() + '}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Timestamp changeCreatedOn;
|
|
||||||
|
|
||||||
@Before
|
|
||||||
public void setUp() {
|
|
||||||
TestTimeUtil.resetWithClockStep(10, TimeUnit.SECONDS);
|
|
||||||
changeCreatedOn = TimeUtil.nowTs();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void naturalSort() {
|
|
||||||
Event e1 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
Event e2 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
Event e3 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
|
|
||||||
for (List<Event> events : Collections2.permutations(events(e1, e2, e3))) {
|
|
||||||
assertSorted(events, events(e1, e2, e3));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void topoSortOneDep() {
|
|
||||||
List<Event> es;
|
|
||||||
|
|
||||||
// Input list is 0,1,2
|
|
||||||
|
|
||||||
// 0 depends on 1 => 1,0,2
|
|
||||||
es = threeEventsOneDep(0, 1);
|
|
||||||
assertSorted(es, events(es, 1, 0, 2));
|
|
||||||
|
|
||||||
// 1 depends on 0 => 0,1,2
|
|
||||||
es = threeEventsOneDep(1, 0);
|
|
||||||
assertSorted(es, events(es, 0, 1, 2));
|
|
||||||
|
|
||||||
// 0 depends on 2 => 1,2,0
|
|
||||||
es = threeEventsOneDep(0, 2);
|
|
||||||
assertSorted(es, events(es, 1, 2, 0));
|
|
||||||
|
|
||||||
// 2 depends on 0 => 0,1,2
|
|
||||||
es = threeEventsOneDep(2, 0);
|
|
||||||
assertSorted(es, events(es, 0, 1, 2));
|
|
||||||
|
|
||||||
// 1 depends on 2 => 0,2,1
|
|
||||||
es = threeEventsOneDep(1, 2);
|
|
||||||
assertSorted(es, events(es, 0, 2, 1));
|
|
||||||
|
|
||||||
// 2 depends on 1 => 0,1,2
|
|
||||||
es = threeEventsOneDep(2, 1);
|
|
||||||
assertSorted(es, events(es, 0, 1, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Event> threeEventsOneDep(int depFromIdx, int depOnIdx) {
|
|
||||||
List<Event> events =
|
|
||||||
Lists.newArrayList(
|
|
||||||
new TestEvent(TimeUtil.nowTs()),
|
|
||||||
new TestEvent(TimeUtil.nowTs()),
|
|
||||||
new TestEvent(TimeUtil.nowTs()));
|
|
||||||
events.get(depFromIdx).addDep(events.get(depOnIdx));
|
|
||||||
return events;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void lastEventDependsOnFirstEvent() {
|
|
||||||
List<Event> events = new ArrayList<>();
|
|
||||||
for (int i = 0; i < 20; i++) {
|
|
||||||
events.add(new TestEvent(TimeUtil.nowTs()));
|
|
||||||
}
|
|
||||||
events.get(events.size() - 1).addDep(events.get(0));
|
|
||||||
assertSorted(events, events);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void firstEventDependsOnLastEvent() {
|
|
||||||
List<Event> events = new ArrayList<>();
|
|
||||||
for (int i = 0; i < 20; i++) {
|
|
||||||
events.add(new TestEvent(TimeUtil.nowTs()));
|
|
||||||
}
|
|
||||||
events.get(0).addDep(events.get(events.size() - 1));
|
|
||||||
|
|
||||||
List<Event> expected = new ArrayList<>();
|
|
||||||
expected.addAll(events.subList(1, events.size()));
|
|
||||||
expected.add(events.get(0));
|
|
||||||
assertSorted(events, expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void topoSortChainOfDeps() {
|
|
||||||
Event e1 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
Event e2 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
Event e3 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
Event e4 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
e1.addDep(e2);
|
|
||||||
e2.addDep(e3);
|
|
||||||
e3.addDep(e4);
|
|
||||||
|
|
||||||
assertSorted(events(e1, e2, e3, e4), events(e4, e3, e2, e1));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void topoSortMultipleDeps() {
|
|
||||||
Event e1 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
Event e2 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
Event e3 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
Event e4 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
e1.addDep(e2);
|
|
||||||
e1.addDep(e4);
|
|
||||||
e2.addDep(e3);
|
|
||||||
|
|
||||||
// Processing 3 pops 2, processing 4 pops 1.
|
|
||||||
assertSorted(events(e2, e3, e1, e4), events(e3, e2, e4, e1));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void topoSortMultipleDepsPreservesNaturalOrder() {
|
|
||||||
Event e1 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
Event e2 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
Event e3 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
Event e4 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
e1.addDep(e4);
|
|
||||||
e2.addDep(e4);
|
|
||||||
e3.addDep(e4);
|
|
||||||
|
|
||||||
// Processing 4 pops 1, 2, 3 in natural order.
|
|
||||||
assertSorted(events(e4, e3, e2, e1), events(e4, e1, e2, e3));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void topoSortCycle() {
|
|
||||||
Event e1 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
Event e2 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
|
|
||||||
// Implementation is not really defined, but infinite looping would be bad.
|
|
||||||
// According to current implementation details, 2 pops 1, 1 pops 2 which was
|
|
||||||
// already seen.
|
|
||||||
assertSorted(events(e2, e1), events(e1, e2));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void topoSortDepNotInInputList() {
|
|
||||||
Event e1 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
Event e2 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
Event e3 = new TestEvent(TimeUtil.nowTs());
|
|
||||||
e1.addDep(e3);
|
|
||||||
|
|
||||||
List<Event> events = events(e2, e1);
|
|
||||||
try {
|
|
||||||
new EventSorter(events).sort();
|
|
||||||
fail("expected IllegalArgumentException");
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// Expected.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Event> events(Event... es) {
|
|
||||||
return Lists.newArrayList(es);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Event> events(List<Event> in, Integer... indexes) {
|
|
||||||
return Stream.of(indexes).map(in::get).collect(toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void assertSorted(List<Event> unsorted, List<Event> expected) {
|
|
||||||
List<Event> actual = new ArrayList<>(unsorted);
|
|
||||||
new EventSorter(actual).sort();
|
|
||||||
assertThat(actual).named("sorted" + unsorted).isEqualTo(expected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user