| // 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.collect.ImmutableSet; |
| import com.google.common.flogger.FluentLogger; |
| import com.google.gerrit.common.Nullable; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.PatchSet; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.exceptions.StorageException; |
| import com.google.gerrit.extensions.client.ChangeKind; |
| import com.google.gerrit.proto.Protos; |
| 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.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.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.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)); |
| } |
| }; |
| } |
| |
| 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(Change change, PatchSet patch) { |
| return getChangeKindInternal(this, 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 Protos.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 = Protos.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(rw, prior, next)) { |
| return ChangeKind.NO_CODE_CHANGE; |
| } |
| return ChangeKind.REWORK; |
| } |
| |
| if (isSameDeltaAndTree(rw, 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) { |
| ImmutableSet<RevCommit> priorRestParents = allExceptFirstParent(prior.getParents()); |
| ImmutableSet<RevCommit> nextRestParents = allExceptFirstParent(next.getParents()); |
| return priorRestParents.equals(nextRestParents); |
| } |
| |
| private static ImmutableSet<RevCommit> allExceptFirstParent(RevCommit[] parents) { |
| return FluentIterable.from(Arrays.asList(parents)).skip(1).toSet(); |
| } |
| |
| private static boolean isSameDeltaAndTree(RevWalk rw, RevCommit prior, RevCommit next) |
| throws IOException { |
| 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++) { |
| // Parse parent commits so that their trees are available. |
| rw.parseCommit(prior.getParent(i)); |
| rw.parseCommit(next.getParent(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); |
| ChangeKind kind = cache.get(key, new Loader(key, repoManager, project, rw, repoConfig)); |
| logger.atFine().log("Change kind of new patch set %s in %s: %s", next.name(), project, kind); |
| return kind; |
| } catch (ExecutionException e) { |
| logger.atWarning().withCause(e).log( |
| "Cannot check change kind of new patch set %s in %s", next.name(), project); |
| return ChangeKind.REWORK; |
| } |
| } |
| |
| @Override |
| public ChangeKind getChangeKind(Change change, PatchSet patch) { |
| return getChangeKindInternal(this, 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.id().get() > 1) { |
| try { |
| Collection<PatchSet> patchSetCollection = change.patchSets(); |
| PatchSet priorPs = patch; |
| for (PatchSet ps : patchSetCollection) { |
| if (ps.id().get() < patch.id().get() |
| && (ps.id().get() > priorPs.id().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, priorPs.commitId(), patch.commitId()); |
| } |
| } catch (StorageException 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.number(), change.getId()); |
| } |
| } |
| logger.atFine().log( |
| "Change kind for patchSet %s of change %s: %s", patch.number(), change.getId(), kind); |
| return kind; |
| } |
| |
| private static ChangeKind getChangeKindInternal( |
| ChangeKindCache cache, |
| 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.id().get() > 1) { |
| try (Repository repo = repoManager.openRepository(change.getProject()); |
| RevWalk rw = new RevWalk(repo)) { |
| kind = |
| getChangeKindInternal( |
| cache, rw, repo.getConfig(), changeDataFactory.create(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.number(), change.getChangeId()); |
| } |
| } |
| logger.atFine().log( |
| "Change kind for patchSet %s of change %s: %s", patch.number(), change.getChangeId(), kind); |
| return kind; |
| } |
| } |