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

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

// 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.base.Objects;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.Weigher;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
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.MergeUtil;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
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.Singleton;
import com.google.inject.name.Named;

import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
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;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Collection;
import java.util.concurrent.ExecutionException;

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)
            .loader(Loader.class);
      }
    };
  }

  @VisibleForTesting
  public static class NoCache implements ChangeKindCache {
    private final boolean useRecursiveMerge;
    private final ChangeData.Factory changeDataFactory;
    private final ProjectCache projectCache;
    private final GitRepositoryManager repoManager;


    @Inject
    NoCache(
        @GerritServerConfig Config serverConfig,
        ChangeData.Factory changeDataFactory,
        ProjectCache projectCache,
        GitRepositoryManager repoManager) {
      this.useRecursiveMerge = MergeUtil.useRecursiveMerge(serverConfig);
      this.changeDataFactory = changeDataFactory;
      this.projectCache = projectCache;
      this.repoManager = repoManager;
    }

    @Override
    public ChangeKind getChangeKind(ProjectState project, Repository repo,
        ObjectId prior, ObjectId next) {
      try {
        return new Loader().load(
            new Key(repo, prior, next, useRecursiveMerge));
      } catch (IOException e) {
        log.warn("Cannot check trivial rebase of new patch set " + next.name()
            + " in " + project.getProject().getName(), e);
        return ChangeKind.REWORK;
      }
    }

    @Override
    public ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch) {
      return getChangeKindInternal(this, db, change, patch, changeDataFactory,
          projectCache, repoManager);
    }
  }

  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 transient Repository repo; // Passed through to loader on miss.

    private Key(Repository repo, 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;
      this.repo = repo;
    }

    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.equal(prior, k.prior)
            && Objects.equal(next, k.next)
            && Objects.equal(strategyName, k.strategyName);
      }
      return false;
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(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();
    }
  }

  @Singleton
  private static class Loader extends CacheLoader {
    @Override
    public ChangeKind load(Key key) throws IOException {
      if (Objects.equal(key.prior, key.next)) {
        return ChangeKind.NO_CODE_CHANGE;
      }

      RevWalk walk = new RevWalk(key.repo);
      try {
        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 (next.getTree() == prior.getTree() && isSameParents(prior, next)) {
            return ChangeKind.NO_CODE_CHANGE;
          } else {
            return ChangeKind.REWORK;
          }
        }

        if (prior.getParentCount() != 1 || next.getParentCount() != 1) {
          // Trivial rebases done by machine only work well on 1 parent.
          return ChangeKind.REWORK;
        }

        if (next.getTree() == prior.getTree() &&
           isSameParents(prior, next)) {
          return ChangeKind.TRIVIAL_REBASE;
        }

        // 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.
        ThreeWayMerger merger = MergeUtil.newThreeWayMerger(
            key.repo, MergeUtil.createDryRunInserter(key.repo), key.strategyName);
        merger.setBase(prior.getParent(0));
        if (merger.merge(next.getParent(0), prior)
            && merger.getResultTreeId().equals(next.getTree())) {
          return ChangeKind.TRIVIAL_REBASE;
        } else {
          return ChangeKind.REWORK;
        }
      } finally {
        key.repo = null;
        walk.release();
      }
    }

    private static boolean isSameParents(RevCommit prior, RevCommit next) {
      if (prior.getParentCount() != next.getParentCount()) {
        return false;
      } else if (prior.getParentCount() == 0) {
        return true;
      }
      return prior.getParent(0).equals(next.getParent(0));
    }
  }

  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 LoadingCache cache;
  private final boolean useRecursiveMerge;
  private final ChangeData.Factory changeDataFactory;
  private final ProjectCache projectCache;
  private final GitRepositoryManager repoManager;

  @Inject
  ChangeKindCacheImpl(
      @GerritServerConfig Config serverConfig,
      @Named(ID_CACHE) LoadingCache cache,
      ChangeData.Factory changeDataFactory,
      ProjectCache projectCache,
      GitRepositoryManager repoManager) {
    this.cache = cache;
    this.useRecursiveMerge = MergeUtil.useRecursiveMerge(serverConfig);
    this.changeDataFactory = changeDataFactory;
    this.projectCache = projectCache;
    this.repoManager = repoManager;
  }

  @Override
  public ChangeKind getChangeKind(ProjectState project, Repository repo,
      ObjectId prior, ObjectId next) {
    try {
      return cache.get(new Key(repo, prior, next, useRecursiveMerge));
    } catch (ExecutionException e) {
      log.warn("Cannot check trivial rebase of new patch set " + next.name()
          + " in " + project.getProject().getName(), e);
      return ChangeKind.REWORK;
    }
  }

  @Override
  public ChangeKind getChangeKind(ReviewDb db, Change change, PatchSet patch) {
    return getChangeKindInternal(this, db, change, patch, changeDataFactory,
        projectCache, repoManager);
  }

  private static ChangeKind getChangeKindInternal(
      ChangeKindCache cache,
      ReviewDb db,
      Change change,
      PatchSet patch,
      ChangeData.Factory changeDataFactory,
      ProjectCache projectCache,
      GitRepositoryManager repoManager) {
    Repository repo = null;
    // 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 {
        ProjectState projectState = projectCache.checkedGet(change.getProject());

        repo = repoManager.openRepository(change.getProject());

        ChangeData cd = changeDataFactory.create(db, change);
        Collection patchSetCollection = cd.patches();
        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(projectState, repo,
                  ObjectId.fromString(priorPs.getRevision().get()),
                  ObjectId.fromString(patch.getRevision().get()));
        }
      } catch (IOException | OrmException 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);
      } finally {
        if (repo != null) {
          repo.close();
        }
      }
    }
    return kind;
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy