summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/change/ChangeKindCacheImpl.java')
-rw-r--r--java/com/google/gerrit/server/change/ChangeKindCacheImpl.java434
1 files changed, 434 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
new file mode 100644
index 0000000000..a6786d86f0
--- /dev/null
+++ b/java/com/google/gerrit/server/change/ChangeKindCacheImpl.java
@@ -0,0 +1,434 @@
+// Copyright (C) 2013 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.change;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.auto.value.AutoValue;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.cache.Cache;
+import com.google.common.cache.Weigher;
+import com.google.common.collect.FluentIterable;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.client.ChangeKind;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.proto.Cache.ChangeKindKeyProto;
+import com.google.gerrit.server.cache.serialize.CacheSerializer;
+import com.google.gerrit.server.cache.serialize.EnumCacheSerializer;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers;
+import com.google.gerrit.server.cache.serialize.ProtoCacheSerializers.ObjectIdConverter;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.InMemoryInserter;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.name.Named;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import org.eclipse.jgit.errors.LargeObjectException;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+public class ChangeKindCacheImpl implements ChangeKindCache {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private static final String ID_CACHE = "change_kind";
+
+ public static Module module() {
+ return new CacheModule() {
+ @Override
+ protected void configure() {
+ bind(ChangeKindCache.class).to(ChangeKindCacheImpl.class);
+ persist(ID_CACHE, Key.class, ChangeKind.class)
+ .maximumWeight(2 << 20)
+ .weigher(ChangeKindWeigher.class)
+ .version(1)
+ .keySerializer(new Key.Serializer())
+ .valueSerializer(new EnumCacheSerializer<>(ChangeKind.class));
+ }
+ };
+ }
+
+ @VisibleForTesting
+ public static class NoCache implements ChangeKindCache {
+ private final boolean useRecursiveMerge;
+ private final ChangeData.Factory changeDataFactory;
+ private final GitRepositoryManager repoManager;
+
+ @Inject
+ NoCache(
+ @GerritServerConfig Config serverConfig,
+ ChangeData.Factory changeDataFactory,
+ GitRepositoryManager repoManager) {
+ this.useRecursiveMerge = MergeUtil.useRecursiveMerge(serverConfig);
+ this.changeDataFactory = changeDataFactory;
+ this.repoManager = repoManager;
+ }
+
+ @Override
+ public ChangeKind getChangeKind(
+ Project.NameKey project,
+ @Nullable RevWalk rw,
+ @Nullable Config repoConfig,
+ ObjectId prior,
+ ObjectId next) {
+ try {
+ Key key = Key.create(prior, next, useRecursiveMerge);
+ return new Loader(key, repoManager, project, rw, repoConfig).call();
+ } catch (IOException e) {
+ logger.atWarning().withCause(e).log(
+ "Cannot check trivial rebase of new patch set %s in %s", next.name(), project);
+ return ChangeKind.REWORK;
+ }
+ }
+
+ @Override
+ public ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch) {
+ return getChangeKindInternal(this, db, change, patch, changeDataFactory, repoManager);
+ }
+
+ @Override
+ public ChangeKind getChangeKind(
+ @Nullable RevWalk rw, @Nullable Config repoConfig, ChangeData cd, PatchSet patch) {
+ return getChangeKindInternal(this, rw, repoConfig, cd, patch);
+ }
+ }
+
+ @AutoValue
+ public abstract static class Key {
+ public static Key create(AnyObjectId prior, AnyObjectId next, String strategyName) {
+ return new AutoValue_ChangeKindCacheImpl_Key(prior.copy(), next.copy(), strategyName);
+ }
+
+ private static Key create(AnyObjectId prior, AnyObjectId next, boolean useRecursiveMerge) {
+ return create(prior, next, MergeUtil.mergeStrategyName(true, useRecursiveMerge));
+ }
+
+ public abstract ObjectId prior();
+
+ public abstract ObjectId next();
+
+ public abstract String strategyName();
+
+ @VisibleForTesting
+ static class Serializer implements CacheSerializer<Key> {
+ @Override
+ public byte[] serialize(Key object) {
+ ObjectIdConverter idConverter = ObjectIdConverter.create();
+ return ProtoCacheSerializers.toByteArray(
+ ChangeKindKeyProto.newBuilder()
+ .setPrior(idConverter.toByteString(object.prior()))
+ .setNext(idConverter.toByteString(object.next()))
+ .setStrategyName(object.strategyName())
+ .build());
+ }
+
+ @Override
+ public Key deserialize(byte[] in) {
+ ChangeKindKeyProto proto =
+ ProtoCacheSerializers.parseUnchecked(ChangeKindKeyProto.parser(), in);
+ ObjectIdConverter idConverter = ObjectIdConverter.create();
+ return create(
+ idConverter.fromByteString(proto.getPrior()),
+ idConverter.fromByteString(proto.getNext()),
+ proto.getStrategyName());
+ }
+ }
+ }
+
+ private static class Loader implements Callable<ChangeKind> {
+ private final Key key;
+ private final GitRepositoryManager repoManager;
+ private final Project.NameKey projectName;
+ private final RevWalk alreadyOpenRw;
+ private final Config repoConfig;
+
+ private Loader(
+ Key key,
+ GitRepositoryManager repoManager,
+ Project.NameKey projectName,
+ @Nullable RevWalk rw,
+ @Nullable Config repoConfig) {
+ checkArgument(
+ (rw == null && repoConfig == null) || (rw != null && repoConfig != null),
+ "must either provide both revwalk/config, or neither; got %s/%s",
+ rw,
+ repoConfig);
+ this.key = key;
+ this.repoManager = repoManager;
+ this.projectName = projectName;
+ this.alreadyOpenRw = rw;
+ this.repoConfig = repoConfig;
+ }
+
+ @SuppressWarnings("resource") // Resources are manually managed.
+ @Override
+ public ChangeKind call() throws IOException {
+ if (Objects.equals(key.prior(), key.next())) {
+ return ChangeKind.NO_CODE_CHANGE;
+ }
+
+ RevWalk rw = alreadyOpenRw;
+ Config config = repoConfig;
+ Repository repo = null;
+ if (alreadyOpenRw == null) {
+ repo = repoManager.openRepository(projectName);
+ rw = new RevWalk(repo);
+ config = repo.getConfig();
+ }
+ try {
+ RevCommit prior = rw.parseCommit(key.prior());
+ rw.parseBody(prior);
+ RevCommit next = rw.parseCommit(key.next());
+ rw.parseBody(next);
+
+ if (!next.getFullMessage().equals(prior.getFullMessage())) {
+ if (isSameDeltaAndTree(prior, next)) {
+ return ChangeKind.NO_CODE_CHANGE;
+ }
+ return ChangeKind.REWORK;
+ }
+
+ if (isSameDeltaAndTree(prior, next)) {
+ return ChangeKind.NO_CHANGE;
+ }
+
+ if (prior.getParentCount() == 0 || next.getParentCount() == 0) {
+ // At this point we have considered all the kinds that could be applicable to root
+ // commits; the remainder of the checks in this method all assume that both commits have
+ // at least one parent.
+ return ChangeKind.REWORK;
+ }
+
+ if ((prior.getParentCount() > 1 || next.getParentCount() > 1)
+ && !onlyFirstParentChanged(prior, next)) {
+ // Trivial rebases done by machine only work well on 1 parent.
+ return ChangeKind.REWORK;
+ }
+
+ // A trivial rebase can be detected by looking for the next commit
+ // having the same tree as would exist when the prior commit is
+ // cherry-picked onto the next commit's new first parent.
+ try (ObjectInserter ins = new InMemoryInserter(rw.getObjectReader())) {
+ ThreeWayMerger merger = MergeUtil.newThreeWayMerger(ins, config, key.strategyName());
+ merger.setBase(prior.getParent(0));
+ if (merger.merge(next.getParent(0), prior)
+ && merger.getResultTreeId().equals(next.getTree())) {
+ if (prior.getParentCount() == 1) {
+ return ChangeKind.TRIVIAL_REBASE;
+ }
+ return ChangeKind.MERGE_FIRST_PARENT_UPDATE;
+ }
+ } catch (LargeObjectException e) {
+ // Some object is too large for the merge attempt to succeed. Assume
+ // it was a rework.
+ }
+ return ChangeKind.REWORK;
+ } finally {
+ if (repo != null) {
+ rw.close();
+ repo.close();
+ }
+ }
+ }
+
+ public static boolean onlyFirstParentChanged(RevCommit prior, RevCommit next) {
+ return !sameFirstParents(prior, next) && sameRestOfParents(prior, next);
+ }
+
+ private static boolean sameFirstParents(RevCommit prior, RevCommit next) {
+ if (prior.getParentCount() == 0) {
+ return next.getParentCount() == 0;
+ }
+ return prior.getParent(0).equals(next.getParent(0));
+ }
+
+ private static boolean sameRestOfParents(RevCommit prior, RevCommit next) {
+ Set<RevCommit> priorRestParents = allExceptFirstParent(prior.getParents());
+ Set<RevCommit> nextRestParents = allExceptFirstParent(next.getParents());
+ return priorRestParents.equals(nextRestParents);
+ }
+
+ private static Set<RevCommit> allExceptFirstParent(RevCommit[] parents) {
+ return FluentIterable.from(Arrays.asList(parents)).skip(1).toSet();
+ }
+
+ private static boolean isSameDeltaAndTree(RevCommit prior, RevCommit next) {
+ if (!Objects.equals(next.getTree(), prior.getTree())) {
+ return false;
+ }
+
+ if (prior.getParentCount() != next.getParentCount()) {
+ return false;
+ } else if (prior.getParentCount() == 0) {
+ return true;
+ }
+
+ // Make sure that the prior/next delta is the same - not just the tree.
+ // This is done by making sure that the parent trees are equal.
+ for (int i = 0; i < prior.getParentCount(); i++) {
+ if (!Objects.equals(next.getParent(i).getTree(), prior.getParent(i).getTree())) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ public static class ChangeKindWeigher implements Weigher<Key, ChangeKind> {
+ @Override
+ public int weigh(Key key, ChangeKind changeKind) {
+ return 16
+ + 2 * 36
+ + 2 * key.strategyName().length() // Size of Key, 64 bit JVM
+ + 2 * changeKind.name().length(); // Size of ChangeKind, 64 bit JVM
+ }
+ }
+
+ private final Cache<Key, ChangeKind> cache;
+ private final boolean useRecursiveMerge;
+ private final ChangeData.Factory changeDataFactory;
+ private final GitRepositoryManager repoManager;
+
+ @Inject
+ ChangeKindCacheImpl(
+ @GerritServerConfig Config serverConfig,
+ @Named(ID_CACHE) Cache<Key, ChangeKind> cache,
+ ChangeData.Factory changeDataFactory,
+ GitRepositoryManager repoManager) {
+ this.cache = cache;
+ this.useRecursiveMerge = MergeUtil.useRecursiveMerge(serverConfig);
+ this.changeDataFactory = changeDataFactory;
+ this.repoManager = repoManager;
+ }
+
+ @Override
+ public ChangeKind getChangeKind(
+ Project.NameKey project,
+ @Nullable RevWalk rw,
+ @Nullable Config repoConfig,
+ ObjectId prior,
+ ObjectId next) {
+ try {
+ Key key = Key.create(prior, next, useRecursiveMerge);
+ return cache.get(key, new Loader(key, repoManager, project, rw, repoConfig));
+ } catch (ExecutionException e) {
+ logger.atWarning().withCause(e).log(
+ "Cannot check trivial rebase of new patch set %s in %s", next.name(), project);
+ return ChangeKind.REWORK;
+ }
+ }
+
+ @Override
+ public ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch) {
+ return getChangeKindInternal(this, db, change, patch, changeDataFactory, repoManager);
+ }
+
+ @Override
+ public ChangeKind getChangeKind(
+ @Nullable RevWalk rw, @Nullable Config repoConfig, ChangeData cd, PatchSet patch) {
+ return getChangeKindInternal(this, rw, repoConfig, cd, patch);
+ }
+
+ private static ChangeKind getChangeKindInternal(
+ ChangeKindCache cache,
+ @Nullable RevWalk rw,
+ @Nullable Config repoConfig,
+ ChangeData change,
+ PatchSet patch) {
+ ChangeKind kind = ChangeKind.REWORK;
+ // Trivial case: if we're on the first patch, we don't need to use
+ // the repository.
+ if (patch.getId().get() > 1) {
+ try {
+ Collection<PatchSet> patchSetCollection = change.patchSets();
+ PatchSet priorPs = patch;
+ for (PatchSet ps : patchSetCollection) {
+ if (ps.getId().get() < patch.getId().get()
+ && (ps.getId().get() > priorPs.getId().get() || priorPs == patch)) {
+ // We only want the previous patch set, so walk until the last one
+ priorPs = ps;
+ }
+ }
+
+ // If we still think the previous patch is the current patch,
+ // we only have one patch set. Return the default.
+ // This can happen if a user creates a draft, uploads a second patch,
+ // and deletes the draft.
+ if (priorPs != patch) {
+ kind =
+ cache.getChangeKind(
+ change.project(),
+ rw,
+ repoConfig,
+ ObjectId.fromString(priorPs.getRevision().get()),
+ ObjectId.fromString(patch.getRevision().get()));
+ }
+ } catch (OrmException e) {
+ // Do nothing; assume we have a complex change
+ logger.atWarning().withCause(e).log(
+ "Unable to get change kind for patchSet %s of change %s",
+ patch.getPatchSetId(), change.getId());
+ }
+ }
+ return kind;
+ }
+
+ private static ChangeKind getChangeKindInternal(
+ ChangeKindCache cache,
+ ReviewDb db,
+ Change change,
+ PatchSet patch,
+ ChangeData.Factory changeDataFactory,
+ GitRepositoryManager repoManager) {
+ // TODO - dborowitz: add NEW_CHANGE type for default.
+ ChangeKind kind = ChangeKind.REWORK;
+ // Trivial case: if we're on the first patch, we don't need to open
+ // the repository.
+ if (patch.getId().get() > 1) {
+ try (Repository repo = repoManager.openRepository(change.getProject());
+ RevWalk rw = new RevWalk(repo)) {
+ kind =
+ getChangeKindInternal(
+ cache, rw, repo.getConfig(), changeDataFactory.create(db, change), patch);
+ } catch (IOException e) {
+ // Do nothing; assume we have a complex change
+ logger.atWarning().withCause(e).log(
+ "Unable to get change kind for patchSet %s of change %s",
+ patch.getPatchSetId(), change.getChangeId());
+ }
+ }
+ return kind;
+ }
+}