All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gerrit.server.change.ChangeKindCacheImpl Maven / Gradle / Ivy

There is a newer version: 3.10.0-rc4
Show newest version
// 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.checkNotNull;
import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;

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.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.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.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
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.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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ChangeKindCacheImpl implements ChangeKindCache {
  private static final Logger log = LoggerFactory.getLogger(ChangeKindCacheImpl.class);

  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);
      }
    };
  }

  @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 Repository repo, ObjectId prior, ObjectId next) {
      try {
        Key key = new Key(prior, next, useRecursiveMerge);
        return new Loader(key, repoManager, project, repo).call();
      } catch (IOException e) {
        log.warn(
            "Cannot check trivial rebase of new patch set " + next.name() + " in " + project, e);
        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 Repository repo, ChangeData cd, PatchSet patch) {
      return getChangeKindInternal(this, repo, cd, patch);
    }
  }

  public static class Key implements Serializable {
    private static final long serialVersionUID = 1L;

    private transient ObjectId prior;
    private transient ObjectId next;
    private transient String strategyName;

    private Key(ObjectId prior, ObjectId next, boolean useRecursiveMerge) {
      checkNotNull(next, "next");
      String strategyName = MergeUtil.mergeStrategyName(true, useRecursiveMerge);
      this.prior = prior.copy();
      this.next = next.copy();
      this.strategyName = strategyName;
    }

    public Key(ObjectId prior, ObjectId next, String strategyName) {
      this.prior = prior;
      this.next = next;
      this.strategyName = strategyName;
    }

    public ObjectId getPrior() {
      return prior;
    }

    public ObjectId getNext() {
      return next;
    }

    public String getStrategyName() {
      return strategyName;
    }

    @Override
    public boolean equals(Object o) {
      if (o instanceof Key) {
        Key k = (Key) o;
        return Objects.equals(prior, k.prior)
            && Objects.equals(next, k.next)
            && Objects.equals(strategyName, k.strategyName);
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Objects.hash(prior, next, strategyName);
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
      writeNotNull(out, prior);
      writeNotNull(out, next);
      out.writeUTF(strategyName);
    }

    private void readObject(ObjectInputStream in) throws IOException {
      prior = readNotNull(in);
      next = readNotNull(in);
      strategyName = in.readUTF();
    }
  }

  private static class Loader implements Callable {
    private final Key key;
    private final GitRepositoryManager repoManager;
    private final Project.NameKey projectName;
    private final Repository alreadyOpenRepo;

    private Loader(
        Key key,
        GitRepositoryManager repoManager,
        Project.NameKey projectName,
        @Nullable Repository alreadyOpenRepo) {
      this.key = key;
      this.repoManager = repoManager;
      this.projectName = projectName;
      this.alreadyOpenRepo = alreadyOpenRepo;
    }

    @Override
    public ChangeKind call() throws IOException {
      if (Objects.equals(key.prior, key.next)) {
        return ChangeKind.NO_CODE_CHANGE;
      }

      Repository repo = alreadyOpenRepo;
      boolean close = false;
      if (repo == null) {
        repo = repoManager.openRepository(projectName);
        close = true;
      }
      try (RevWalk walk = new RevWalk(repo)) {
        RevCommit prior = walk.parseCommit(key.prior);
        walk.parseBody(prior);
        RevCommit next = walk.parseCommit(key.next);
        walk.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(repo)) {
          ThreeWayMerger merger = MergeUtil.newThreeWayMerger(repo, ins, 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 (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 priorRestParents = allExceptFirstParent(prior.getParents());
      Set nextRestParents = allExceptFirstParent(next.getParents());
      return priorRestParents.equals(nextRestParents);
    }

    private static Set allExceptFirstParent(RevCommit[] parents) {
      return FluentIterable.from(Arrays.asList(parents)).skip(1).toSet();
    }

    private static boolean isSameDeltaAndTree(RevCommit prior, RevCommit next) {
      if (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 (next.getParent(i).getTree() != prior.getParent(i).getTree()) {
          return false;
        }
      }
      return true;
    }
  }

  public static class ChangeKindWeigher implements Weigher {
    @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 cache;
  private final boolean useRecursiveMerge;
  private final ChangeData.Factory changeDataFactory;
  private final GitRepositoryManager repoManager;

  @Inject
  ChangeKindCacheImpl(
      @GerritServerConfig Config serverConfig,
      @Named(ID_CACHE) Cache 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 Repository repo, ObjectId prior, ObjectId next) {
    try {
      Key key = new Key(prior, next, useRecursiveMerge);
      return cache.get(key, new Loader(key, repoManager, project, repo));
    } catch (ExecutionException e) {
      log.warn("Cannot check trivial rebase of new patch set " + next.name() + " in " + project, e);
      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 Repository repo, ChangeData cd, PatchSet patch) {
    return getChangeKindInternal(this, repo, cd, patch);
  }

  private static ChangeKind getChangeKindInternal(
      ChangeKindCache cache, @Nullable Repository repo, 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 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(),
                  repo,
                  ObjectId.fromString(priorPs.getRevision().get()),
                  ObjectId.fromString(patch.getRevision().get()));
        }
      } catch (OrmException e) {
        // Do nothing; assume we have a complex change
        log.warn(
            "Unable to get change kind for patchSet "
                + patch.getPatchSetId()
                + "of change "
                + change.getId(),
            e);
      }
    }
    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())) {
        kind = getChangeKindInternal(cache, repo, changeDataFactory.create(db, change), patch);
      } catch (IOException e) {
        // Do nothing; assume we have a complex change
        log.warn(
            "Unable to get change kind for patchSet "
                + patch.getPatchSetId()
                + "of change "
                + change.getChangeId(),
            e);
      }
    }
    return kind;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy