diff options
Diffstat (limited to 'java/com/google/gerrit/server/change/ChangeKindCacheImpl.java')
-rw-r--r-- | java/com/google/gerrit/server/change/ChangeKindCacheImpl.java | 434 |
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; + } +} |