blob: fc4c1d084daf272a0baa83785edef085c4c09768 [file] [log] [blame]
// 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.query.change;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.flogger.LazyArgs.lazy;
import static com.google.gerrit.server.project.ProjectCache.noSuchProject;
import static java.util.concurrent.TimeUnit.MINUTES;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.SubmitTypeRecord;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.query.PostFilterPredicate;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeQueryBuilder.Arguments;
import com.google.gerrit.server.submit.SubmitDryRun;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
public class ConflictsPredicate {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
// UI code may depend on this string, so use caution when changing.
protected static final String TOO_MANY_FILES = "too many files to find conflicts";
private ConflictsPredicate() {}
public static Predicate<ChangeData> create(Arguments args, String value, Change c)
throws QueryParseException {
ChangeData cd;
List<String> files;
try {
cd = args.changeDataFactory.create(c);
files = cd.currentFilePaths();
} catch (StorageException e) {
warnWithOccasionalStackTrace(
e,
"Error constructing conflicts predicates for change %s in %s",
c.getId(),
c.getProject());
return ChangeIndexPredicate.none();
}
if (3 + files.size() > args.indexConfig.maxTerms()) {
// Short-circuit with a nice error message if we exceed the index
// backend's term limit. This assumes that "conflicts:foo" is the entire
// query; if there are more terms in the input, we might not
// short-circuit here, which will result in a more generic error message
// later on in the query parsing.
throw new QueryParseException(TOO_MANY_FILES);
}
List<Predicate<ChangeData>> filePredicates = new ArrayList<>(files.size());
for (String file : files) {
filePredicates.add(ChangePredicates.path(file));
}
List<Predicate<ChangeData>> and = new ArrayList<>(5);
and.add(ChangePredicates.project(c.getProject()));
and.add(ChangePredicates.ref(c.getDest().branch()));
and.add(Predicate.not(ChangePredicates.idStr(c.getId())));
and.add(Predicate.or(filePredicates));
ChangeDataCache changeDataCache = new ChangeDataCache(cd, args.projectCache);
and.add(new CheckConflict(value, args, c, changeDataCache));
return Predicate.and(and);
}
private static final class CheckConflict extends PostFilterPredicate<ChangeData> {
private final Arguments args;
private final BranchNameKey dest;
private final ChangeDataCache changeDataCache;
CheckConflict(String value, Arguments args, Change c, ChangeDataCache changeDataCache) {
super(ChangeQueryBuilder.FIELD_CONFLICTS, value);
this.args = args;
this.dest = c.getDest();
this.changeDataCache = changeDataCache;
}
@Override
public boolean match(ChangeData object) {
Change.Id id = object.getId();
Project.NameKey otherProject = null;
ObjectId other = null;
try {
Change otherChange = object.change();
if (otherChange == null || !otherChange.getDest().equals(dest)) {
return false;
}
otherProject = otherChange.getProject();
SubmitTypeRecord str = object.submitTypeRecord();
if (!str.isOk()) {
return false;
}
ProjectState projectState;
try {
projectState = changeDataCache.getProjectState();
} catch (NoSuchProjectException e) {
return false;
}
other = object.currentPatchSet().commitId();
ConflictKey conflictsKey =
ConflictKey.create(
changeDataCache.getTestAgainst(),
other,
str.type,
projectState.is(BooleanProjectConfig.USE_CONTENT_MERGE));
return args.conflictsCache.get(conflictsKey, new Loader(object, changeDataCache, args));
} catch (StorageException | ExecutionException | UncheckedExecutionException e) {
ObjectId finalOther = other;
warnWithOccasionalStackTrace(
e,
"Merge failure checking conflicts of change %s in %s (%s): %s",
id,
firstNonNull(otherProject, "unknown project"),
lazy(() -> finalOther != null ? finalOther.name() : "unknown commit"),
e.getMessage());
return false;
}
}
@Override
public int getCost() {
return 5;
}
}
static class ChangeDataCache {
private final ChangeData cd;
private final ProjectCache projectCache;
private ObjectId testAgainst;
private ProjectState projectState;
private Set<ObjectId> alreadyAccepted;
ChangeDataCache(ChangeData cd, ProjectCache projectCache) {
this.cd = cd;
this.projectCache = projectCache;
}
ObjectId getTestAgainst() {
if (testAgainst == null) {
testAgainst = cd.currentPatchSet().commitId();
}
return testAgainst;
}
ProjectState getProjectState() throws NoSuchProjectException {
if (projectState == null) {
projectState = projectCache.get(cd.project()).orElseThrow(noSuchProject(cd.project()));
}
return projectState;
}
Set<ObjectId> getAlreadyAccepted(Repository repo) throws IOException {
if (alreadyAccepted == null) {
alreadyAccepted = SubmitDryRun.getAlreadyAccepted(repo);
}
return alreadyAccepted;
}
}
private static void warnWithOccasionalStackTrace(Throwable cause, String format, Object... args) {
logger.atWarning().logVarargs(format, args);
logger
.atWarning()
.withCause(cause)
.atMostEvery(1, MINUTES)
.logVarargs("(Re-logging with stack trace) " + format, args);
}
private static class Loader implements Callable<Boolean> {
private final ChangeData changeData;
private final ConflictsPredicate.ChangeDataCache changeDataCache;
private final ChangeQueryBuilder.Arguments args;
private Loader(
ChangeData changeData,
ConflictsPredicate.ChangeDataCache changeDataCache,
ChangeQueryBuilder.Arguments args) {
this.changeData = changeData;
this.changeDataCache = changeDataCache;
this.args = args;
}
@Override
public Boolean call() throws Exception {
Change otherChange = changeData.change();
ObjectId other = changeData.currentPatchSet().commitId();
try (Repository repo = args.repoManager.openRepository(otherChange.getProject());
CodeReviewCommit.CodeReviewRevWalk rw = CodeReviewCommit.newRevWalk(repo)) {
return !args.submitDryRun.run(
null,
changeData.submitTypeRecord().type,
repo,
rw,
otherChange.getDest(),
changeDataCache.getTestAgainst(),
other,
getAlreadyAccepted(repo, rw));
} catch (NoSuchProjectException | IOException e) {
warnWithOccasionalStackTrace(
e,
"Failure when loading conflicts of change %s in %s (%s): %s",
lazy(changeData::getId),
lazy(() -> firstNonNull(otherChange.getProject(), "unknown project")),
lazy(() -> other != null ? other.name() : "unknown commit"),
e.getMessage());
return false;
}
}
private Set<RevCommit> getAlreadyAccepted(Repository repo, RevWalk rw) {
try {
Set<RevCommit> accepted = new HashSet<>();
SubmitDryRun.addCommits(changeDataCache.getAlreadyAccepted(repo), rw, accepted);
ObjectId tip = changeDataCache.getTestAgainst();
if (tip != null) {
accepted.add(rw.parseCommit(tip));
}
return accepted;
} catch (StorageException | IOException e) {
throw new StorageException("Failed to determine already accepted commits.", e);
}
}
}
}