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.TrackingFootersProvider;
 | 
			
		||||
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.schema.NotesMigrationSchemaFactory;
 | 
			
		||||
import com.google.gerrit.server.schema.ReviewDbFactory;
 | 
			
		||||
@@ -83,7 +81,6 @@ class InMemoryTestingDatabaseModule extends LifecycleModule {
 | 
			
		||||
    bind(schemaFactory).to(NotesMigrationSchemaFactory.class);
 | 
			
		||||
    bind(Key.get(schemaFactory, ReviewDbFactory.class)).to(InMemoryDatabase.class);
 | 
			
		||||
    bind(InMemoryDatabase.class).in(SINGLETON);
 | 
			
		||||
    bind(ChangeBundleReader.class).to(GwtormChangeBundleReader.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.DisableReverseDnsLookup;
 | 
			
		||||
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.GitUploadPackGroups;
 | 
			
		||||
import com.google.gerrit.server.config.SysExecutorModule;
 | 
			
		||||
@@ -87,16 +86,13 @@ import com.google.inject.util.Providers;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import org.eclipse.jgit.lib.Config;
 | 
			
		||||
 | 
			
		||||
/** Module for programs that perform batch operations on a site. */
 | 
			
		||||
public class BatchProgramModule extends FactoryModule {
 | 
			
		||||
  private final Config cfg;
 | 
			
		||||
  private final Module reviewDbModule;
 | 
			
		||||
 | 
			
		||||
  @Inject
 | 
			
		||||
  BatchProgramModule(@GerritServerConfig Config cfg, PerThreadReviewDbModule reviewDbModule) {
 | 
			
		||||
    this.cfg = cfg;
 | 
			
		||||
  BatchProgramModule(PerThreadReviewDbModule reviewDbModule) {
 | 
			
		||||
    this.reviewDbModule = reviewDbModule;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -168,7 +164,7 @@ public class BatchProgramModule extends FactoryModule {
 | 
			
		||||
    install(new H2CacheModule());
 | 
			
		||||
    install(new ExternalIdModule());
 | 
			
		||||
    install(new GroupModule());
 | 
			
		||||
    install(new NoteDbModule(cfg));
 | 
			
		||||
    install(new NoteDbModule());
 | 
			
		||||
    install(AccountCacheImpl.module());
 | 
			
		||||
    install(GroupCacheImpl.module());
 | 
			
		||||
    install(GroupIncludeCacheImpl.module());
 | 
			
		||||
 
 | 
			
		||||
@@ -246,7 +246,7 @@ public class GerritGlobalModule extends FactoryModule {
 | 
			
		||||
    install(new GitModule());
 | 
			
		||||
    install(new GroupDbModule());
 | 
			
		||||
    install(new GroupModule());
 | 
			
		||||
    install(new NoteDbModule(cfg));
 | 
			
		||||
    install(new NoteDbModule());
 | 
			
		||||
    install(new PrologModule());
 | 
			
		||||
    install(new DefaultSubmitRule.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.CacheBuilder;
 | 
			
		||||
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.name.Names;
 | 
			
		||||
import org.eclipse.jgit.lib.Config;
 | 
			
		||||
 | 
			
		||||
public class NoteDbModule extends FactoryModule {
 | 
			
		||||
  private final Config cfg;
 | 
			
		||||
  private final boolean useTestBindings;
 | 
			
		||||
 | 
			
		||||
  static NoteDbModule forTest(Config cfg) {
 | 
			
		||||
    return new NoteDbModule(cfg, true);
 | 
			
		||||
  static NoteDbModule forTest() {
 | 
			
		||||
    return new NoteDbModule(true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public NoteDbModule(Config cfg) {
 | 
			
		||||
    this(cfg, false);
 | 
			
		||||
  public NoteDbModule() {
 | 
			
		||||
    this(false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private NoteDbModule(Config cfg, boolean useTestBindings) {
 | 
			
		||||
    this.cfg = cfg;
 | 
			
		||||
  private NoteDbModule(boolean useTestBindings) {
 | 
			
		||||
    this.useTestBindings = useTestBindings;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -57,53 +47,7 @@ public class NoteDbModule extends FactoryModule {
 | 
			
		||||
 | 
			
		||||
    if (!useTestBindings) {
 | 
			
		||||
      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 {
 | 
			
		||||
      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>>() {})
 | 
			
		||||
          .annotatedWith(Names.named(ChangeNotesCache.CACHE_NAME))
 | 
			
		||||
          .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.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.SchemaFactory;
 | 
			
		||||
import com.google.inject.Key;
 | 
			
		||||
@@ -35,6 +33,5 @@ public class DatabaseModule extends FactoryModule {
 | 
			
		||||
            () -> {
 | 
			
		||||
              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.GroupSchemaDefinitions;
 | 
			
		||||
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.NotesMigration;
 | 
			
		||||
import com.google.gerrit.server.patch.DiffExecutor;
 | 
			
		||||
@@ -198,7 +196,6 @@ public class InMemoryModule extends FactoryModule {
 | 
			
		||||
    bind(ListeningExecutorService.class)
 | 
			
		||||
        .annotatedWith(ChangeUpdateExecutor.class)
 | 
			
		||||
        .toInstance(MoreExecutors.newDirectExecutorService());
 | 
			
		||||
    bind(ChangeBundleReader.class).to(GwtormChangeBundleReader.class);
 | 
			
		||||
    bind(SecureStore.class).to(DefaultSecureStore.class);
 | 
			
		||||
 | 
			
		||||
    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.index.account.AccountIndexer;
 | 
			
		||||
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.RefPattern;
 | 
			
		||||
import com.google.gerrit.server.query.account.InternalAccountQuery;
 | 
			
		||||
@@ -468,7 +467,7 @@ public class AccountIT extends AbstractDaemonTest {
 | 
			
		||||
      RevCommit c = rw.parseCommit(ref.getObjectId());
 | 
			
		||||
      long timestampDiffMs =
 | 
			
		||||
          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.
 | 
			
		||||
      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 DefaultUrlFormatter.Module());
 | 
			
		||||
                install(NoteDbModule.forTest(testConfig));
 | 
			
		||||
                install(NoteDbModule.forTest());
 | 
			
		||||
                bind(AllUsersName.class).toProvider(AllUsersNameProvider.class);
 | 
			
		||||
                bind(String.class).annotatedWith(GerritServerId.class).toInstance("gerrit");
 | 
			
		||||
                bind(GitRepositoryManager.class).toInstance(repoManager);
 | 
			
		||||
@@ -172,11 +172,6 @@ public abstract class AbstractChangeNotesTest extends GerritBaseTests {
 | 
			
		||||
                        () -> {
 | 
			
		||||
                          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