blob: 56fa64b8ce6902969cbfe9c8e02854aff54e3b98 [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.plugins.checks;
import com.google.auto.value.AutoValue;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.index.query.IndexPredicate;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.plugins.checks.api.BlockingCondition;
import com.google.gerrit.plugins.checks.api.CheckerStatus;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeQueryProcessor;
import com.google.gerrit.server.query.change.ChangeStatusPredicate;
import com.google.gerrit.server.query.change.ProjectPredicate;
import com.google.gerrit.server.update.RetryHelper;
import com.google.gerrit.server.update.RetryHelper.ActionType;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
import java.sql.Timestamp;
import java.util.List;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
/** Definition of a checker. */
@AutoValue
public abstract class Checker {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
* Returns the UUID of the checker.
*
* <p>The UUID is unique across all checkers.
*
* @return UUID
*/
public abstract CheckerUuid getUuid();
/**
* Returns the display name of the checker.
*
* <p>Checkers may not have a name, in this case {@link Optional#empty()} is returned.
*
* @return display name of the checker
*/
public abstract Optional<String> getName();
/**
* Returns the description of the checker.
*
* <p>Checkers may not have a description, in this case {@link Optional#empty()} is returned.
*
* @return the description of the checker
*/
public abstract Optional<String> getDescription();
/**
* Returns the URL of the checker.
*
* <p>Checkers may not have a URL, in this case {@link Optional#empty()} is returned.
*
* @return the URL of the checker
*/
public abstract Optional<String> getUrl();
/**
* Returns the repository to which the checker applies.
*
* <p>The repository is the exact name of a repository (no prefix, no regexp).
*
* @return the repository to which the checker applies
*/
public abstract Project.NameKey getRepository();
/**
* Returns the status of the checker.
*
* @return the status of the checker.
*/
public abstract CheckerStatus getStatus();
/**
* Returns the blocking conditions for the checker.
*
* @return the blocking conditions for the checker.
*/
public abstract ImmutableSortedSet<BlockingCondition> getBlockingConditions();
/**
* Returns the query for the checker.
*
* <p>If set, represents a limited change query that all relevant changes will match.
*
* @return the query for the checker.
*/
public abstract Optional<String> getQuery();
/**
* Returns the creation timestamp of the checker.
*
* @return the creation timestamp
*/
public abstract Timestamp getCreated();
/**
* Returns the timestamp of when the checker was last updated.
*
* @return the last updated timestamp
*/
public abstract Timestamp getUpdated();
/**
* Returns the ref state of the checker.
*
* @return the ref state
*/
public abstract ObjectId getRefState();
public abstract Builder toBuilder();
public static Builder builder() {
return new AutoValue_Checker.Builder();
}
public boolean isDisabled() {
return CheckerStatus.DISABLED == getStatus();
}
public boolean isCheckerRelevant(ChangeData cd, ChangeQueryBuilder changeQueryBuilder)
throws OrmException {
if (!getQuery().isPresent()) {
return cd.change().isNew();
}
Predicate<ChangeData> predicate;
try {
predicate = createQueryPredicate(changeQueryBuilder);
} catch (ConfigInvalidException e) {
logger.atWarning().withCause(e).log("skipping invalid query for checker %s", getUuid());
return false;
}
return predicate.asMatchable().match(cd);
}
public List<ChangeData> queryMatchingChanges(
RetryHelper retryHelper,
ChangeQueryBuilder changeQueryBuilder,
Provider<ChangeQueryProcessor> changeQueryProcessorProvider)
throws ConfigInvalidException, OrmException {
return executeIndexQueryWithRetry(
retryHelper, changeQueryProcessorProvider, createQueryPredicate(changeQueryBuilder));
}
private Predicate<ChangeData> createQueryPredicate(ChangeQueryBuilder changeQueryBuilder)
throws ConfigInvalidException {
Predicate<ChangeData> predicate = new ProjectPredicate(getRepository().get());
if (getQuery().isPresent()) {
String query = getQuery().get();
Predicate<ChangeData> predicateForQuery;
try {
predicateForQuery = changeQueryBuilder.parse(query);
} catch (QueryParseException e) {
throw new ConfigInvalidException(
String.format("change query of checker %s is invalid: %s", getUuid(), query), e);
}
if (!predicateForQuery.isMatchable()) {
// Assuming nobody modified the query behind Gerrit's back, this is programmer error:
// CheckerQuery should not be able to produce non-matchable queries.
throw new ConfigInvalidException(
String.format("change query of checker %s is non-matchable: %s", getUuid(), query));
}
predicate = Predicate.and(predicate, predicateForQuery);
}
if (!hasStatusPredicate(predicate)) {
predicate = Predicate.and(ChangeStatusPredicate.open(), predicate);
}
return predicate;
}
private static boolean hasStatusPredicate(Predicate<ChangeData> predicate) {
if (predicate instanceof IndexPredicate) {
return ((IndexPredicate<ChangeData>) predicate)
.getField()
.getName()
.equals(ChangeField.STATUS.getName());
}
return predicate.getChildren().stream().anyMatch(Checker::hasStatusPredicate);
}
// TODO(ekempin): Retrying the query should be done by ChangeQueryProcessor.
private List<ChangeData> executeIndexQueryWithRetry(
RetryHelper retryHelper,
Provider<ChangeQueryProcessor> changeQueryProcessorProvider,
Predicate<ChangeData> predicate)
throws OrmException {
try {
return retryHelper.execute(
ActionType.INDEX_QUERY,
() -> changeQueryProcessorProvider.get().query(predicate).entities(),
OrmException.class::isInstance);
} catch (Exception e) {
Throwables.throwIfUnchecked(e);
Throwables.throwIfInstanceOf(e, OrmException.class);
throw new OrmException(e);
}
}
/**
* Checks whether a {@link Checker} is required for submission or not.
*
* @return true if the {@link Checker} required for submission.
*/
public boolean isRequired() {
ImmutableSet<BlockingCondition> blockingConditions = getBlockingConditions();
if (blockingConditions.isEmpty()) {
return false;
} else if (blockingConditions.size() > 1
|| !blockingConditions.contains(BlockingCondition.STATE_NOT_PASSING)) {
// When a new blocking condition is introduced, this needs to be adjusted to respect that.
String errorMessage = String.format("illegal blocking conditions %s", blockingConditions);
throw new IllegalStateException(errorMessage);
}
return true;
}
/** A builder for an {@link Checker}. */
@AutoValue.Builder
public abstract static class Builder {
public abstract Builder setUuid(CheckerUuid uuid);
public abstract CheckerUuid getUuid();
public abstract Builder setName(String name);
public abstract Builder setDescription(String description);
public abstract Builder setUrl(String url);
public abstract Builder setRepository(Project.NameKey repository);
public abstract Builder setStatus(CheckerStatus status);
public abstract Builder setBlockingConditions(Iterable<BlockingCondition> enumList);
public abstract Builder setQuery(String query);
public abstract Builder setCreated(Timestamp created);
public abstract Builder setUpdated(Timestamp updated);
public abstract Builder setRefState(ObjectId refState);
public abstract Checker build();
}
}