blob: 3910393239e1935c60dd821b32732fc7cd22d82e [file] [log] [blame]
// Copyright (C) 2019 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.git;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Throwables;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.server.cache.CacheModule;
import com.google.gerrit.server.cache.proto.Cache;
import com.google.gerrit.server.cache.proto.Cache.PureRevertKeyProto;
import com.google.gerrit.server.cache.serialize.BooleanCacheSerializer;
import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
import com.google.gerrit.server.cache.serialize.ProtobufSerializer;
import com.google.gerrit.server.logging.Metadata;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.project.ProjectCache;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Singleton;
import com.google.inject.name.Named;
import com.google.protobuf.ByteString;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ExecutionException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.errors.InvalidObjectIdException;
import org.eclipse.jgit.errors.MissingObjectException;
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.eclipse.jgit.util.io.DisabledOutputStream;
/** Computes and caches if a change is a pure revert of another change. */
@Singleton
public class PureRevertCache {
private static final String ID_CACHE = "pure_revert";
public static Module module() {
return new CacheModule() {
@Override
protected void configure() {
persist(ID_CACHE, Cache.PureRevertKeyProto.class, Boolean.class)
.maximumWeight(100)
.loader(Loader.class)
.version(1)
.keySerializer(new ProtobufSerializer<>(Cache.PureRevertKeyProto.parser()))
.valueSerializer(BooleanCacheSerializer.INSTANCE);
}
};
}
private final LoadingCache<PureRevertKeyProto, Boolean> cache;
private final ChangeNotes.Factory notesFactory;
@Inject
PureRevertCache(
@Named(ID_CACHE) LoadingCache<PureRevertKeyProto, Boolean> cache,
ChangeNotes.Factory notesFactory) {
this.cache = cache;
this.notesFactory = notesFactory;
}
/**
* Returns {@code true} if {@code claimedRevert} is a pure (clean) revert of the change that is
* referenced in {@link Change#getRevertOf()}.
*
* @return {@code true} if {@code claimedRevert} is a pure (clean) revert.
* @throws IOException if there was a problem with the storage layer
* @throws BadRequestException if there is a problem with the provided {@link ChangeNotes}
*/
public boolean isPureRevert(ChangeNotes claimedRevert) throws IOException, BadRequestException {
if (claimedRevert.getChange().getRevertOf() == null) {
throw new BadRequestException("revertOf not set");
}
ChangeNotes claimedOriginal =
notesFactory.createChecked(
claimedRevert.getProjectName(), claimedRevert.getChange().getRevertOf());
return isPureRevert(
claimedRevert.getProjectName(),
claimedRevert.getCurrentPatchSet().commitId(),
claimedOriginal.getCurrentPatchSet().commitId());
}
/**
* Returns {@code true} if {@code claimedRevert} is a pure (clean) revert of {@code
* claimedOriginal}.
*
* @return {@code true} if {@code claimedRevert} is a pure (clean) revert of {@code
* claimedOriginal}.
* @throws IOException if there was a problem with the storage layer
* @throws BadRequestException if there is a problem with the provided {@link ObjectId}s
*/
public boolean isPureRevert(
Project.NameKey project, ObjectId claimedRevert, ObjectId claimedOriginal)
throws IOException, BadRequestException {
try {
return cache.get(key(project, claimedRevert, claimedOriginal));
} catch (ExecutionException e) {
Throwables.throwIfInstanceOf(e.getCause(), BadRequestException.class);
throw new IOException(e);
}
}
@VisibleForTesting
static PureRevertKeyProto key(
Project.NameKey project, ObjectId claimedRevert, ObjectId claimedOriginal) {
ByteString original = ObjectIdConverter.create().toByteString(claimedOriginal);
ByteString revert = ObjectIdConverter.create().toByteString(claimedRevert);
return PureRevertKeyProto.newBuilder()
.setProject(project.get())
.setClaimedOriginal(original)
.setClaimedRevert(revert)
.build();
}
static class Loader extends CacheLoader<PureRevertKeyProto, Boolean> {
private final GitRepositoryManager repoManager;
private final MergeUtil.Factory mergeUtilFactory;
private final ProjectCache projectCache;
@Inject
Loader(
GitRepositoryManager repoManager,
MergeUtil.Factory mergeUtilFactory,
ProjectCache projectCache) {
this.repoManager = repoManager;
this.mergeUtilFactory = mergeUtilFactory;
this.projectCache = projectCache;
}
@Override
public Boolean load(PureRevertKeyProto key) throws BadRequestException, IOException {
try (TraceContext.TraceTimer ignored =
TraceContext.newTimer(
"Loading pure revert",
Metadata.builder().cacheKey(key.toString()).projectName(key.getProject()).build())) {
ObjectId original = ObjectIdConverter.create().fromByteString(key.getClaimedOriginal());
ObjectId revert = ObjectIdConverter.create().fromByteString(key.getClaimedRevert());
Project.NameKey project = Project.nameKey(key.getProject());
try (Repository repo = repoManager.openRepository(project);
ObjectInserter oi = repo.newObjectInserter();
RevWalk rw = new RevWalk(repo)) {
RevCommit claimedOriginalCommit;
try {
claimedOriginalCommit = rw.parseCommit(original);
} catch (InvalidObjectIdException | MissingObjectException e) {
throw new BadRequestException("invalid object ID", e);
}
if (claimedOriginalCommit.getParentCount() == 0) {
throw new BadRequestException("can't check against initial commit");
}
RevCommit claimedRevertCommit = rw.parseCommit(revert);
if (claimedRevertCommit.getParentCount() == 0) {
return false;
}
// Rebase claimed revert onto claimed original
ThreeWayMerger merger =
mergeUtilFactory
.create(projectCache.get(project).orElseThrow(illegalState(project)))
.newThreeWayMerger(oi, repo.getConfig());
merger.setBase(claimedRevertCommit.getParent(0));
boolean success = merger.merge(claimedRevertCommit, claimedOriginalCommit);
if (!success || merger.getResultTreeId() == null) {
// Merge conflict during rebase
return false;
}
// Any differences between claimed original's parent and the rebase result indicate that
// the claimedRevert is not a pure revert but made content changes
try (DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE)) {
df.setReader(oi.newReader(), repo.getConfig());
List<DiffEntry> entries =
df.scan(claimedOriginalCommit.getParent(0), merger.getResultTreeId());
return entries.isEmpty();
}
}
}
}
}
}