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