Merge changes from topic "plugin-pre-submit-validation"

* changes:
  Add SubmitRule extension point
  Extract the Prolog evaluator out of SubmitRuleEvaluator
  Adapt SubmitRuleEvaluator for the new SubmitRule interface
  Remove the Prolog reductions counter
  Remove unused SubmitRuleEvaluator#setPatchSet
  Change the Presubmit handling with multiple SubmitRecords
  Add tests for Prolog's handling of SubmitRecords
  Add SubmitRequirement to hold the presubmit conditions not met
This commit is contained in:
Patrick Hiesel
2018-03-14 10:06:05 +00:00
committed by Gerrit Code Review
31 changed files with 1300 additions and 591 deletions

View File

@@ -18,15 +18,20 @@ import com.google.gerrit.reviewdb.client.Account;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/** Describes the state required to submit a change. */
/** Describes the state and edits required to submit a change. */
public class SubmitRecord {
public static Optional<SubmitRecord> findOkRecord(Collection<SubmitRecord> in) {
if (in == null) {
return Optional.empty();
public static boolean allRecordsOK(Collection<SubmitRecord> in) {
if (in == null || in.isEmpty()) {
// If the list is null or empty, it means that this Gerrit installation does not
// have any form of validation rules.
// Hence, the permission system should be used to determine if the change can be merged
// or not.
return true;
}
return in.stream().filter(r -> r.status == Status.OK).findFirst();
// The change can be submitted, unless at least one plugin prevents it.
return in.stream().noneMatch(r -> r.status != Status.OK);
}
public enum Status {
@@ -36,7 +41,7 @@ public class SubmitRecord {
/** The change is ready for submission. */
OK,
/** The change is missing a required label. */
/** Something is preventing this change from being submitted. */
NOT_READY,
/** The change has been closed. */
@@ -55,6 +60,7 @@ public class SubmitRecord {
public Status status;
public List<Label> labels;
public List<SubmitRequirement> requirements;
public String errorMessage;
public static class Label {
@@ -140,6 +146,14 @@ public class SubmitRecord {
delimiter = ", ";
}
}
sb.append("],[");
if (requirements != null) {
String delimiter = "";
for (SubmitRequirement requirement : requirements) {
sb.append(delimiter).append(requirement);
delimiter = ", ";
}
}
sb.append(']');
return sb.toString();
}
@@ -150,13 +164,14 @@ public class SubmitRecord {
SubmitRecord r = (SubmitRecord) o;
return Objects.equals(status, r.status)
&& Objects.equals(labels, r.labels)
&& Objects.equals(errorMessage, r.errorMessage);
&& Objects.equals(errorMessage, r.errorMessage)
&& Objects.equals(requirements, r.requirements);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(status, labels, errorMessage);
return Objects.hash(status, labels, errorMessage, requirements);
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (C) 2018 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.common.data;
import static java.util.Objects.requireNonNull;
import com.google.gerrit.common.Nullable;
import java.util.Objects;
import java.util.Optional;
/** Describes a requirement to submit a change. */
public final class SubmitRequirement {
private final String shortReason;
private final String fullReason;
@Nullable private final String label;
public SubmitRequirement(String shortReason, String fullReason, @Nullable String label) {
this.shortReason = requireNonNull(shortReason);
this.fullReason = requireNonNull(fullReason);
this.label = label;
}
public String shortReason() {
return shortReason;
}
public String fullReason() {
return fullReason;
}
public Optional<String> label() {
return Optional.ofNullable(label);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o instanceof SubmitRequirement) {
SubmitRequirement that = (SubmitRequirement) o;
return Objects.equals(shortReason, that.shortReason)
&& Objects.equals(fullReason, that.fullReason)
&& Objects.equals(label, that.label);
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(shortReason, fullReason, label);
}
@Override
public String toString() {
return "SubmitRequirement{"
+ "shortReason='"
+ shortReason
+ '\''
+ ", fullReason='"
+ fullReason
+ '\''
+ ", label='"
+ label
+ '\''
+ '}';
}
}

View File

@@ -157,7 +157,6 @@ public class BatchProgramModule extends FactoryModule {
install(new ExternalIdModule());
install(new GroupModule());
install(new NoteDbModule(cfg));
install(new PrologModule());
install(AccountCacheImpl.module());
install(GroupCacheImpl.module());
install(GroupIncludeCacheImpl.module());
@@ -169,7 +168,10 @@ public class BatchProgramModule extends FactoryModule {
factory(CapabilityCollection.Factory.class);
factory(ChangeData.AssistedFactory.class);
factory(ProjectState.Factory.class);
// Submit rule evaluator
factory(SubmitRuleEvaluator.Factory.class);
install(new PrologModule());
bind(ChangeJson.Factory.class).toProvider(Providers.<ChangeJson.Factory>of(null));
bind(EventUtil.class).toProvider(Providers.<EventUtil>of(null));

View File

@@ -657,7 +657,7 @@ public class ChangeJson {
}
private boolean submittable(ChangeData cd) {
return SubmitRecord.findOkRecord(cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT)).isPresent();
return SubmitRecord.allRecordsOK(cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT));
}
private List<SubmitRecord> submitRecords(ChangeData cd) {

View File

@@ -177,6 +177,7 @@ import com.google.gerrit.server.restapi.config.ConfigRestModule;
import com.google.gerrit.server.restapi.group.GroupModule;
import com.google.gerrit.server.rules.PrologModule;
import com.google.gerrit.server.rules.RulesCache;
import com.google.gerrit.server.rules.SubmitRule;
import com.google.gerrit.server.ssh.SshAddressesModule;
import com.google.gerrit.server.tools.ToolsCatalog;
import com.google.gerrit.server.update.BatchUpdate;
@@ -391,6 +392,7 @@ public class GerritGlobalModule extends FactoryModule {
DynamicSet.setOf(binder(), ActionVisitor.class);
DynamicItem.itemOf(binder(), MergeSuperSetComputation.class);
DynamicItem.itemOf(binder(), ProjectNameLockManager.class);
DynamicSet.setOf(binder(), SubmitRule.class);
DynamicMap.mapOf(binder(), MailFilter.class);
bind(MailFilter.class).annotatedWith(Exports.named("ListMailFilter")).to(ListMailFilter.class);

View File

@@ -14,6 +14,10 @@
package com.google.gerrit.server.data;
/**
* Represents a {@link com.google.gerrit.common.data.SubmitRecord.Label} that does not depend on
* Gerrit internal classes, to be serialized.
*/
public class SubmitLabelAttribute {
public String label;
public String status;

View File

@@ -16,7 +16,12 @@ package com.google.gerrit.server.data;
import java.util.List;
/**
* Represents a {@link com.google.gerrit.common.data.SubmitRecord} that does not depend on Gerrit
* internal classes, to be serialized.
*/
public class SubmitRecordAttribute {
public String status;
public List<SubmitLabelAttribute> labels;
public List<SubmitRequirementAttribute> requirements;
}

View File

@@ -0,0 +1,25 @@
// Copyright (C) 2018 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.data;
/**
* Represents a {@link com.google.gerrit.common.data.SubmitRequirement} that does not depend on
* Gerrit internal classes, to be serialized
*/
public class SubmitRequirementAttribute {
public String shortReason;
public String fullReason;
public String label;
}

View File

@@ -23,6 +23,7 @@ import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.LabelType;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.index.IndexConfig;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Branch;
@@ -52,6 +53,7 @@ import com.google.gerrit.server.data.PatchSetCommentAttribute;
import com.google.gerrit.server.data.RefUpdateAttribute;
import com.google.gerrit.server.data.SubmitLabelAttribute;
import com.google.gerrit.server.data.SubmitRecordAttribute;
import com.google.gerrit.server.data.SubmitRequirementAttribute;
import com.google.gerrit.server.data.TrackingIdAttribute;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.patch.PatchList;
@@ -229,6 +231,7 @@ public class EventFactory {
sa.status = submitRecord.status.name();
if (submitRecord.status != SubmitRecord.Status.RULE_ERROR) {
addSubmitRecordLabels(submitRecord, sa);
addSubmitRecordRequirements(submitRecord, sa);
}
ca.submitRecords.add(sa);
}
@@ -253,6 +256,19 @@ public class EventFactory {
}
}
private void addSubmitRecordRequirements(SubmitRecord submitRecord, SubmitRecordAttribute sa) {
if (submitRecord.requirements != null && !submitRecord.requirements.isEmpty()) {
sa.requirements = new ArrayList<>();
for (SubmitRequirement req : submitRecord.requirements) {
SubmitRequirementAttribute re = new SubmitRequirementAttribute();
re.shortReason = req.shortReason();
re.fullReason = req.fullReason();
re.label = req.label().orElse(null);
sa.requirements.add(re);
}
}
}
public void addDependencies(RevWalk rw, ChangeAttribute ca, Change change, PatchSet currentPs) {
if (change == null || currentPs == null) {
return;

View File

@@ -291,7 +291,7 @@ public class MergeOp implements AutoCloseable {
throw new ResourceConflictException("missing current patch set for change " + cd.getId());
}
List<SubmitRecord> results = getSubmitRecords(cd, allowClosed);
if (SubmitRecord.findOkRecord(results).isPresent()) {
if (SubmitRecord.allRecordsOK(results)) {
// Rules supplied a valid solution.
return;
} else if (results.isEmpty()) {
@@ -303,6 +303,9 @@ public class MergeOp implements AutoCloseable {
for (SubmitRecord record : results) {
switch (record.status) {
case OK:
break;
case CLOSED:
throw new ResourceConflictException("change is closed");
@@ -313,7 +316,6 @@ public class MergeOp implements AutoCloseable {
throw new ResourceConflictException(describeLabels(cd, record.labels));
case FORCED:
case OK:
default:
throw new IllegalStateException(
String.format(

View File

@@ -36,7 +36,9 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Table;
import com.google.common.primitives.Longs;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitRequirement;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.SchemaUtil;
import com.google.gerrit.reviewdb.client.Account;
@@ -657,8 +659,15 @@ public class ChangeField {
Integer appliedBy;
}
static class StoredRequirement {
String shortReason;
String fullReason;
@Nullable String label;
}
SubmitRecord.Status status;
List<StoredLabel> labels;
List<StoredRequirement> requirements;
String errorMessage;
StoredSubmitRecord(SubmitRecord rec) {
@@ -674,6 +683,16 @@ public class ChangeField {
this.labels.add(sl);
}
}
if (rec.requirements != null) {
this.requirements = new ArrayList<>(rec.requirements.size());
for (SubmitRequirement requirement : rec.requirements) {
StoredRequirement sr = new StoredRequirement();
sr.shortReason = requirement.shortReason();
sr.fullReason = requirement.fullReason();
sr.label = requirement.label().orElse(null);
this.requirements.add(sr);
}
}
}
private SubmitRecord toSubmitRecord() {
@@ -690,6 +709,15 @@ public class ChangeField {
rec.labels.add(srl);
}
}
if (requirements != null) {
rec.requirements = new ArrayList<>(requirements.size());
for (StoredRequirement requirement : requirements) {
SubmitRequirement sr =
new SubmitRequirement(
requirement.shortReason, requirement.fullReason, requirement.label);
rec.requirements.add(sr);
}
}
return rec;
}
}

View File

@@ -728,6 +728,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
msg.append('\n');
}
}
// TODO(maximeg) We might want to list plugins that validated this submission.
}
}

View File

@@ -14,13 +14,14 @@
package com.google.gerrit.server.project;
@SuppressWarnings("serial")
public class RuleEvalException extends Exception {
private static final long serialVersionUID = 1L;
public RuleEvalException(String message) {
super(message);
}
RuleEvalException(String message, Throwable cause) {
public RuleEvalException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -14,38 +14,21 @@
package com.google.gerrit.server.project;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.Emails;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.PrologEnvironment;
import com.google.gerrit.server.rules.StoredValues;
import com.google.gerrit.server.rules.PrologRule;
import com.google.gerrit.server.rules.SubmitRule;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.googlecode.prolog_cafe.exceptions.CompileException;
import com.googlecode.prolog_cafe.exceptions.ReductionLimitException;
import com.googlecode.prolog_cafe.lang.IntegerTerm;
import com.googlecode.prolog_cafe.lang.ListTerm;
import com.googlecode.prolog_cafe.lang.Prolog;
import com.googlecode.prolog_cafe.lang.StructureTerm;
import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;
import com.googlecode.prolog_cafe.lang.VariableTerm;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -55,9 +38,31 @@ import org.slf4j.LoggerFactory;
*/
public class SubmitRuleEvaluator {
private static final Logger log = LoggerFactory.getLogger(SubmitRuleEvaluator.class);
private static final String DEFAULT_MSG = "Error evaluating project rules, check server log";
private final ProjectCache projectCache;
private final PrologRule prologRule;
private final DynamicSet<SubmitRule> submitRules;
private final SubmitRuleOptions opts;
public interface Factory {
/** Returns a new {@link SubmitRuleEvaluator} with the specified options */
SubmitRuleEvaluator create(SubmitRuleOptions options);
}
@Inject
private SubmitRuleEvaluator(
ProjectCache projectCache,
PrologRule prologRule,
DynamicSet<SubmitRule> submitRules,
@Assisted SubmitRuleOptions options) {
this.projectCache = projectCache;
this.prologRule = prologRule;
this.submitRules = submitRules;
this.opts = options;
}
public static List<SubmitRecord> defaultRuleError() {
return createRuleError(DEFAULT_MSG);
}
@@ -73,144 +78,26 @@ public class SubmitRuleEvaluator {
return SubmitTypeRecord.error(DEFAULT_MSG);
}
/**
* Exception thrown when the label term of a submit record unexpectedly didn't contain a user
* term.
*/
private static class UserTermExpected extends Exception {
private static final long serialVersionUID = 1L;
UserTermExpected(SubmitRecord.Label label) {
super(String.format("A label with the status %s must contain a user.", label.toString()));
}
}
public interface Factory {
SubmitRuleEvaluator create(ChangeData cd);
}
private final AccountCache accountCache;
private final Accounts accounts;
private final Emails emails;
private final ProjectCache projectCache;
private final ChangeData cd;
private SubmitRuleOptions.Builder optsBuilder = SubmitRuleOptions.builder();
private SubmitRuleOptions opts;
private Change change;
private PatchSet patchSet;
private boolean logErrors = true;
private long reductionsConsumed;
private ProjectState projectState;
private Term submitRule;
@Inject
SubmitRuleEvaluator(
AccountCache accountCache,
Accounts accounts,
Emails emails,
ProjectCache projectCache,
@Assisted ChangeData cd) {
this.accountCache = accountCache;
this.accounts = accounts;
this.emails = emails;
this.projectCache = projectCache;
this.cd = cd;
}
/**
* @return immutable snapshot of options configured so far. If neither {@link #getSubmitRule()}
* nor {@link #getSubmitType()} have been called yet, state within this instance is still
* mutable, so may change before evaluation. The instance's options are frozen at evaluation
* time.
*/
public SubmitRuleOptions getOptions() {
if (opts != null) {
return opts;
}
return optsBuilder.build();
}
public SubmitRuleEvaluator setOptions(SubmitRuleOptions opts) {
checkNotStarted();
if (opts != null) {
optsBuilder = opts.toBuilder();
} else {
optsBuilder = SubmitRuleOptions.builder();
}
return this;
}
/**
* @param ps patch set of the change to evaluate. If not set, the current patch set will be loaded
* from {@link #evaluate()} or {@link #getSubmitType}.
* @return this
*/
public SubmitRuleEvaluator setPatchSet(PatchSet ps) {
checkArgument(
ps.getId().getParentKey().equals(cd.getId()),
"Patch set %s does not match change %s",
ps.getId(),
cd.getId());
patchSet = ps;
return this;
}
/**
* @param allow whether to allow {@link #evaluate()} on closed changes.
* @return this
*/
public SubmitRuleEvaluator setAllowClosed(boolean allow) {
checkNotStarted();
optsBuilder.allowClosed(allow);
return this;
}
/**
* @param skip if true, submit filter will not be applied.
* @return this
*/
public SubmitRuleEvaluator setSkipSubmitFilters(boolean skip) {
checkNotStarted();
optsBuilder.skipFilters(skip);
return this;
}
/**
* @param rule custom rule to use, or null to use refs/meta/config:rules.pl.
* @return this
*/
public SubmitRuleEvaluator setRule(@Nullable String rule) {
checkNotStarted();
optsBuilder.rule(rule);
return this;
}
/**
* @param log whether to log error messages in addition to returning error records. If true, error
* record messages will be less descriptive.
*/
public SubmitRuleEvaluator setLogErrors(boolean log) {
logErrors = log;
return this;
}
/** @return Prolog reductions consumed during evaluation. */
public long getReductionsConsumed() {
return reductionsConsumed;
}
/**
* Evaluate the submit rules.
*
* @return List of {@link SubmitRecord} objects returned from the evaluated rules, including any
* errors.
* @param cd ChangeData to evaluate
*/
public List<SubmitRecord> evaluate() {
initOptions();
public List<SubmitRecord> evaluate(ChangeData cd) {
Change change;
ProjectState projectState;
try {
init();
change = cd.change();
if (change == null) {
throw new OrmException("Change not found");
}
projectState = projectCache.get(cd.project());
if (projectState == null) {
throw new NoSuchProjectException(cd.project());
}
} catch (OrmException | NoSuchProjectException e) {
return ruleError("Error looking up change " + cd.getId(), e);
}
@@ -221,138 +108,16 @@ public class SubmitRuleEvaluator {
return Collections.singletonList(rec);
}
List<Term> results;
try {
results =
evaluateImpl(
"locate_submit_rule", "can_submit", "locate_submit_filter", "filter_submit_results");
} catch (RuleEvalException e) {
return ruleError(e.getMessage(), e);
}
if (results.isEmpty()) {
// This should never occur. A well written submit rule will always produce
// at least one result informing the caller of the labels that are
// required for this change to be submittable. Each label will indicate
// whether or not that is actually possible given the permissions.
return ruleError(
String.format(
"Submit rule '%s' for change %s of %s has no solution.",
getSubmitRuleName(), cd.getId(), getProjectName()));
}
return resultsToSubmitRecord(getSubmitRule(), results);
}
/**
* Convert the results from Prolog Cafe's format to Gerrit's common format.
*
* <p>can_submit/1 terminates when an ok(P) record is found. Therefore walk the results backwards,
* using only that ok(P) record if it exists. This skips partial results that occur early in the
* output. Later after the loop the out collection is reversed to restore it to the original
* ordering.
*/
private List<SubmitRecord> resultsToSubmitRecord(Term submitRule, List<Term> results) {
List<SubmitRecord> out = new ArrayList<>(results.size());
for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
Term submitRecord = results.get(resultIdx);
SubmitRecord rec = new SubmitRecord();
out.add(rec);
if (!(submitRecord instanceof StructureTerm) || 1 != submitRecord.arity()) {
return invalidResult(submitRule, submitRecord);
}
if ("ok".equals(submitRecord.name())) {
rec.status = SubmitRecord.Status.OK;
} else if ("not_ready".equals(submitRecord.name())) {
rec.status = SubmitRecord.Status.NOT_READY;
} else {
return invalidResult(submitRule, submitRecord);
}
// Unpack the one argument. This should also be a structure with one
// argument per label that needs to be reported on to the caller.
//
submitRecord = submitRecord.arg(0);
if (!(submitRecord instanceof StructureTerm)) {
return invalidResult(submitRule, submitRecord);
}
rec.labels = new ArrayList<>(submitRecord.arity());
for (Term state : ((StructureTerm) submitRecord).args()) {
if (!(state instanceof StructureTerm)
|| 2 != state.arity()
|| !"label".equals(state.name())) {
return invalidResult(submitRule, submitRecord);
}
SubmitRecord.Label lbl = new SubmitRecord.Label();
rec.labels.add(lbl);
lbl.label = state.arg(0).name();
Term status = state.arg(1);
try {
if ("ok".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.OK;
appliedBy(lbl, status);
} else if ("reject".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.REJECT;
appliedBy(lbl, status);
} else if ("need".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.NEED;
} else if ("may".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.MAY;
} else if ("impossible".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
} else {
return invalidResult(submitRule, submitRecord);
}
} catch (UserTermExpected e) {
return invalidResult(submitRule, submitRecord, e.getMessage());
}
}
if (rec.status == SubmitRecord.Status.OK) {
break;
}
}
Collections.reverse(out);
return out;
}
private List<SubmitRecord> invalidResult(Term rule, Term record, String reason) {
return ruleError(
String.format(
"Submit rule %s for change %s of %s output invalid result: %s%s",
rule,
cd.getId(),
getProjectName(),
record,
(reason == null ? "" : ". Reason: " + reason)));
}
private List<SubmitRecord> invalidResult(Term rule, Term record) {
return invalidResult(rule, record, null);
}
private List<SubmitRecord> ruleError(String err) {
return ruleError(err, null);
// We evaluate all the plugin-defined evaluators,
// and then we collect the results in one list.
return StreamSupport.stream(submitRules.spliterator(), false)
.map(s -> s.evaluate(cd, opts))
.flatMap(Collection::stream)
.collect(Collectors.toList());
}
private List<SubmitRecord> ruleError(String err, Exception e) {
if (logErrors) {
if (opts.logErrors()) {
if (e == null) {
log.error(err);
} else {
@@ -367,73 +132,24 @@ public class SubmitRuleEvaluator {
* Evaluate the submit type rules to get the submit type.
*
* @return record from the evaluated rules.
* @param cd
*/
public SubmitTypeRecord getSubmitType() {
initOptions();
public SubmitTypeRecord getSubmitType(ChangeData cd) {
ProjectState projectState;
try {
init();
} catch (OrmException | NoSuchProjectException e) {
projectState = projectCache.get(cd.project());
if (projectState == null) {
throw new NoSuchProjectException(cd.project());
}
} catch (NoSuchProjectException e) {
return typeError("Error looking up change " + cd.getId(), e);
}
List<Term> results;
try {
results =
evaluateImpl(
"locate_submit_type",
"get_submit_type",
"locate_submit_type_filter",
"filter_submit_type_results");
} catch (RuleEvalException e) {
return typeError(e.getMessage(), e);
}
if (results.isEmpty()) {
// Should never occur for a well written rule
return typeError(
"Submit rule '"
+ getSubmitRuleName()
+ "' for change "
+ cd.getId()
+ " of "
+ getProjectName()
+ " has no solution.");
}
Term typeTerm = results.get(0);
if (!(typeTerm instanceof SymbolTerm)) {
return typeError(
"Submit rule '"
+ getSubmitRuleName()
+ "' for change "
+ cd.getId()
+ " of "
+ getProjectName()
+ " did not return a symbol.");
}
String typeName = ((SymbolTerm) typeTerm).name();
try {
return SubmitTypeRecord.OK(SubmitType.valueOf(typeName.toUpperCase()));
} catch (IllegalArgumentException e) {
return typeError(
"Submit type rule "
+ getSubmitRule()
+ " for change "
+ cd.getId()
+ " of "
+ getProjectName()
+ " output invalid result: "
+ typeName);
}
}
private SubmitTypeRecord typeError(String err) {
return typeError(err, null);
return prologRule.getSubmitType(cd, opts);
}
private SubmitTypeRecord typeError(String err, Exception e) {
if (logErrors) {
if (opts.logErrors()) {
if (e == null) {
log.error(err);
} else {
@@ -443,194 +159,4 @@ public class SubmitRuleEvaluator {
}
return SubmitTypeRecord.error(err);
}
private List<Term> evaluateImpl(
String userRuleLocatorName,
String userRuleWrapperName,
String filterRuleLocatorName,
String filterRuleWrapperName)
throws RuleEvalException {
PrologEnvironment env = getPrologEnvironment();
try {
Term sr = env.once("gerrit", userRuleLocatorName, new VariableTerm());
List<Term> results = new ArrayList<>();
try {
for (Term[] template : env.all("gerrit", userRuleWrapperName, sr, new VariableTerm())) {
results.add(template[1]);
}
} catch (ReductionLimitException err) {
throw new RuleEvalException(
String.format(
"%s on change %d of %s", err.getMessage(), cd.getId().get(), getProjectName()));
} catch (RuntimeException err) {
throw new RuleEvalException(
String.format(
"Exception calling %s on change %d of %s", sr, cd.getId().get(), getProjectName()),
err);
} finally {
reductionsConsumed = env.getReductions();
}
Term resultsTerm = toListTerm(results);
if (!opts.skipFilters()) {
resultsTerm =
runSubmitFilters(resultsTerm, env, filterRuleLocatorName, filterRuleWrapperName);
}
List<Term> r;
if (resultsTerm instanceof ListTerm) {
r = new ArrayList<>();
for (Term t = resultsTerm; t instanceof ListTerm; ) {
ListTerm l = (ListTerm) t;
r.add(l.car().dereference());
t = l.cdr().dereference();
}
} else {
r = Collections.emptyList();
}
submitRule = sr;
return r;
} finally {
env.close();
}
}
private PrologEnvironment getPrologEnvironment() throws RuleEvalException {
PrologEnvironment env;
try {
if (opts.rule() == null) {
env = projectState.newPrologEnvironment();
} else {
env = projectState.newPrologEnvironment("stdin", new StringReader(opts.rule()));
}
} catch (CompileException err) {
String msg;
if (opts.rule() == null) {
msg = String.format("Cannot load rules.pl for %s: %s", getProjectName(), err.getMessage());
} else {
msg = err.getMessage();
}
throw new RuleEvalException(msg, err);
}
env.set(StoredValues.ACCOUNTS, accounts);
env.set(StoredValues.ACCOUNT_CACHE, accountCache);
env.set(StoredValues.EMAILS, emails);
env.set(StoredValues.REVIEW_DB, cd.db());
env.set(StoredValues.CHANGE_DATA, cd);
env.set(StoredValues.PROJECT_STATE, projectState);
return env;
}
private Term runSubmitFilters(
Term results,
PrologEnvironment env,
String filterRuleLocatorName,
String filterRuleWrapperName)
throws RuleEvalException {
PrologEnvironment childEnv = env;
for (ProjectState parentState : projectState.parents()) {
PrologEnvironment parentEnv;
try {
parentEnv = parentState.newPrologEnvironment();
} catch (CompileException err) {
throw new RuleEvalException("Cannot consult rules.pl for " + parentState.getName(), err);
}
parentEnv.copyStoredValues(childEnv);
Term filterRule = parentEnv.once("gerrit", filterRuleLocatorName, new VariableTerm());
try {
Term[] template =
parentEnv.once(
"gerrit", filterRuleWrapperName, filterRule, results, new VariableTerm());
results = template[2];
} catch (ReductionLimitException err) {
throw new RuleEvalException(
String.format(
"%s on change %d of %s",
err.getMessage(), cd.getId().get(), parentState.getName()));
} catch (RuntimeException err) {
throw new RuleEvalException(
String.format(
"Exception calling %s on change %d of %s",
filterRule, cd.getId().get(), parentState.getName()),
err);
} finally {
reductionsConsumed += env.getReductions();
}
childEnv = parentEnv;
}
return results;
}
private static Term toListTerm(List<Term> terms) {
Term list = Prolog.Nil;
for (int i = terms.size() - 1; i >= 0; i--) {
list = new ListTerm(terms.get(i), list);
}
return list;
}
private void appliedBy(SubmitRecord.Label label, Term status) throws UserTermExpected {
if (status instanceof StructureTerm && status.arity() == 1) {
Term who = status.arg(0);
if (isUser(who)) {
label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue());
} else {
throw new UserTermExpected(label);
}
}
}
private static boolean isUser(Term who) {
return who instanceof StructureTerm
&& who.arity() == 1
&& who.name().equals("user")
&& who.arg(0) instanceof IntegerTerm;
}
public Term getSubmitRule() {
checkState(submitRule != null, "getSubmitRule() invalid before evaluation");
return submitRule;
}
public String getSubmitRuleName() {
return submitRule != null ? submitRule.toString() : "<unknown rule>";
}
private void checkNotStarted() {
checkState(opts == null, "cannot set options after starting evaluation");
}
private void initOptions() {
if (opts == null) {
opts = optsBuilder.build();
optsBuilder = null;
}
}
private void init() throws OrmException, NoSuchProjectException {
if (change == null) {
change = cd.change();
if (change == null) {
throw new OrmException("No change found");
}
}
if (projectState == null) {
projectState = projectCache.get(change.getProject());
if (projectState == null) {
throw new NoSuchProjectException(change.getProject());
}
}
if (patchSet == null) {
patchSet = cd.currentPatchSet();
if (patchSet == null) {
throw new OrmException("No patch set found");
}
}
}
private String getProjectName() {
return projectState.getName();
}
}

View File

@@ -25,17 +25,28 @@ import com.google.gerrit.common.Nullable;
*/
@AutoValue
public abstract class SubmitRuleOptions {
private static final SubmitRuleOptions defaults =
new AutoValue_SubmitRuleOptions.Builder()
.allowClosed(false)
.skipFilters(false)
.logErrors(true)
.rule(null)
.build();
public static SubmitRuleOptions defaults() {
return defaults;
}
public static Builder builder() {
return new AutoValue_SubmitRuleOptions.Builder()
.allowClosed(false)
.skipFilters(false)
.rule(null);
return defaults.toBuilder();
}
public abstract boolean allowClosed();
public abstract boolean skipFilters();
public abstract boolean logErrors();
@Nullable
public abstract String rule();
@@ -49,6 +60,8 @@ public abstract class SubmitRuleOptions {
public abstract SubmitRuleOptions.Builder rule(@Nullable String rule);
public abstract SubmitRuleOptions.Builder logErrors(boolean logErrors);
public abstract SubmitRuleOptions build();
}
}

View File

@@ -943,7 +943,7 @@ public class ChangeData {
if (!lazyLoad) {
return Collections.emptyList();
}
records = submitRuleEvaluatorFactory.create(this).setOptions(options).evaluate();
records = submitRuleEvaluatorFactory.create(options).evaluate(this);
submitRecords.put(options, records);
}
return records;
@@ -960,7 +960,8 @@ public class ChangeData {
public SubmitTypeRecord submitTypeRecord() {
if (submitTypeRecord == null) {
submitTypeRecord = submitRuleEvaluatorFactory.create(this).getSubmitType();
submitTypeRecord =
submitRuleEvaluatorFactory.create(SubmitRuleOptions.defaults()).getSubmitType(this);
}
return submitTypeRecord;
}

View File

@@ -31,6 +31,7 @@ import com.google.gerrit.server.data.QueryStatsAttribute;
import com.google.gerrit.server.events.EventFactory;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gson.Gson;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -249,8 +250,8 @@ public class OutputStreamQuery {
}
if (includeSubmitRecords) {
eventFactory.addSubmitRecords(
c, submitRuleEvaluatorFactory.create(d).setAllowClosed(true).evaluate());
SubmitRuleOptions options = SubmitRuleOptions.builder().allowClosed(true).build();
eventFactory.addSubmitRecords(c, submitRuleEvaluatorFactory.create(options).evaluate(d));
}
if (includeCommitMessage) {

View File

@@ -35,6 +35,7 @@ import com.google.gerrit.server.index.change.ChangeIndexer;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -70,7 +71,7 @@ public class Mergeable implements RestReadView<RevisionResource> {
private final Provider<ReviewDb> db;
private final ChangeIndexer indexer;
private final MergeabilityCache cache;
private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
private final SubmitRuleEvaluator submitRuleEvaluator;
@Inject
Mergeable(
@@ -89,7 +90,7 @@ public class Mergeable implements RestReadView<RevisionResource> {
this.db = db;
this.indexer = indexer;
this.cache = cache;
this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
submitRuleEvaluator = submitRuleEvaluatorFactory.create(SubmitRuleOptions.defaults());
}
public void setOtherBranches(boolean otherBranches) {
@@ -112,7 +113,7 @@ public class Mergeable implements RestReadView<RevisionResource> {
}
ChangeData cd = changeDataFactory.create(db.get(), resource.getNotes());
result.submitType = getSubmitType(cd, ps);
result.submitType = getSubmitType(cd);
try (Repository git = gitManager.openRepository(change.getProject())) {
ObjectId commit = toId(ps);
@@ -144,9 +145,8 @@ public class Mergeable implements RestReadView<RevisionResource> {
return result;
}
private SubmitType getSubmitType(ChangeData cd, PatchSet patchSet) throws OrmException {
SubmitTypeRecord rec =
submitRuleEvaluatorFactory.create(cd).setPatchSet(patchSet).getSubmitType();
private SubmitType getSubmitType(ChangeData cd) throws OrmException {
SubmitTypeRecord rec = submitRuleEvaluator.getSubmitType(cd);
if (rec.status != SubmitTypeRecord.Status.OK) {
throw new OrmException("Submit type rule failed: " + rec);
}

View File

@@ -33,6 +33,7 @@ import com.google.gerrit.server.permissions.LabelPermission;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
@@ -49,7 +50,7 @@ public class ReviewerJson {
private final ChangeData.Factory changeDataFactory;
private final ApprovalsUtil approvalsUtil;
private final AccountLoader.Factory accountLoaderFactory;
private final SubmitRuleEvaluator.Factory submitRuleEvaluatorFactory;
private final SubmitRuleEvaluator submitRuleEvaluator;
@Inject
ReviewerJson(
@@ -64,7 +65,7 @@ public class ReviewerJson {
this.changeDataFactory = changeDataFactory;
this.approvalsUtil = approvalsUtil;
this.accountLoaderFactory = accountLoaderFactory;
this.submitRuleEvaluatorFactory = submitRuleEvaluatorFactory;
submitRuleEvaluator = submitRuleEvaluatorFactory.create(SubmitRuleOptions.defaults());
}
public List<ReviewerInfo> format(Collection<ReviewerResource> rsrcs)
@@ -124,7 +125,7 @@ public class ReviewerJson {
// do not exist in the DB.
PatchSet ps = cd.currentPatchSet();
if (ps != null) {
for (SubmitRecord rec : submitRuleEvaluatorFactory.create(cd).evaluate()) {
for (SubmitRecord rec : submitRuleEvaluator.evaluate(cd)) {
if (rec.labels == null) {
continue;
}

View File

@@ -26,6 +26,7 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.account.AccountLoader;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.RulesCache;
import com.google.gwtorm.server.OrmException;
@@ -70,24 +71,22 @@ public class TestSubmitRule implements RestModifyView<RevisionResource, TestSubm
throw new AuthException("project rules are disabled");
}
input.filters = MoreObjects.firstNonNull(input.filters, filters);
SubmitRuleEvaluator evaluator =
submitRuleEvaluatorFactory.create(changeDataFactory.create(db.get(), rsrc.getNotes()));
List<SubmitRecord> records =
evaluator
.setPatchSet(rsrc.getPatchSet())
.setLogErrors(false)
.setSkipSubmitFilters(input.filters == Filters.SKIP)
.setRule(input.rule)
.evaluate();
SubmitRuleOptions opts =
SubmitRuleOptions.builder()
.skipFilters(input.filters == Filters.SKIP)
.rule(input.rule)
.logErrors(false)
.build();
ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
List<SubmitRecord> records = submitRuleEvaluatorFactory.create(opts).evaluate(cd);
List<Record> out = Lists.newArrayListWithCapacity(records.size());
AccountLoader accounts = accountInfoFactory.create(true);
for (SubmitRecord r : records) {
out.add(new Record(r, accounts));
}
if (!out.isEmpty()) {
out.get(0).prologReductionCount = evaluator.getReductionsConsumed();
}
accounts.fill();
return out;
}
@@ -100,7 +99,6 @@ public class TestSubmitRule implements RestModifyView<RevisionResource, TestSubm
Map<String, None> need;
Map<String, AccountInfo> may;
Map<String, None> impossible;
Long prologReductionCount;
Record(SubmitRecord r, AccountLoader accounts) {
this.status = r.status;

View File

@@ -26,6 +26,7 @@ import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.RulesCache;
import com.google.gwtorm.server.OrmException;
@@ -64,19 +65,20 @@ public class TestSubmitType implements RestModifyView<RevisionResource, TestSubm
throw new AuthException("project rules are disabled");
}
input.filters = MoreObjects.firstNonNull(input.filters, filters);
SubmitRuleEvaluator evaluator =
submitRuleEvaluatorFactory.create(changeDataFactory.create(db.get(), rsrc.getNotes()));
SubmitTypeRecord rec =
evaluator
.setPatchSet(rsrc.getPatchSet())
.setLogErrors(false)
.setSkipSubmitFilters(input.filters == Filters.SKIP)
.setRule(input.rule)
.getSubmitType();
SubmitRuleOptions opts =
SubmitRuleOptions.builder()
.logErrors(false)
.skipFilters(input.filters == Filters.SKIP)
.rule(input.rule)
.build();
SubmitRuleEvaluator evaluator = submitRuleEvaluatorFactory.create(opts);
ChangeData cd = changeDataFactory.create(db.get(), rsrc.getNotes());
SubmitTypeRecord rec = evaluator.getSubmitType(cd);
if (rec.status != SubmitTypeRecord.Status.OK) {
throw new BadRequestException(
String.format("rule %s produced invalid result: %s", evaluator.getSubmitRuleName(), rec));
throw new BadRequestException(String.format("rule produced invalid result: %s", rec));
}
return rec.type;

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.server.rules;
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.registration.DynamicSet;
@@ -22,6 +23,9 @@ public class PrologModule extends FactoryModule {
protected void configure() {
install(new EnvironmentModule());
bind(PrologEnvironment.Args.class);
factory(PrologRuleEvaluator.Factory.class);
bind(SubmitRule.class).annotatedWith(Exports.named("PrologRule")).to(PrologRule.class);
}
static class EnvironmentModule extends FactoryModule {

View File

@@ -0,0 +1,48 @@
// Copyright (C) 2018 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.rules;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.PrologRuleEvaluator.Factory;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.Collection;
@Singleton
public class PrologRule implements SubmitRule {
private final Factory factory;
@Inject
private PrologRule(PrologRuleEvaluator.Factory factory) {
this.factory = factory;
}
@Override
public Collection<SubmitRecord> evaluate(ChangeData cd, SubmitRuleOptions opts) {
return getEvaluator(cd, opts).evaluate();
}
private PrologRuleEvaluator getEvaluator(ChangeData cd, SubmitRuleOptions opts) {
return factory.create(cd, opts);
}
public SubmitTypeRecord getSubmitType(ChangeData cd, SubmitRuleOptions opts) {
return getEvaluator(cd, opts).getSubmitType();
}
}

View File

@@ -0,0 +1,519 @@
// Copyright (C) 2012 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.rules;
import static com.google.gerrit.server.project.SubmitRuleEvaluator.createRuleError;
import static com.google.gerrit.server.project.SubmitRuleEvaluator.defaultRuleError;
import static com.google.gerrit.server.project.SubmitRuleEvaluator.defaultTypeError;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.data.SubmitTypeRecord;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.Emails;
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.project.RuleEvalException;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gwtorm.server.OrmException;
import com.google.inject.assistedinject.Assisted;
import com.google.inject.assistedinject.AssistedInject;
import com.googlecode.prolog_cafe.exceptions.CompileException;
import com.googlecode.prolog_cafe.exceptions.ReductionLimitException;
import com.googlecode.prolog_cafe.lang.IntegerTerm;
import com.googlecode.prolog_cafe.lang.ListTerm;
import com.googlecode.prolog_cafe.lang.Prolog;
import com.googlecode.prolog_cafe.lang.StructureTerm;
import com.googlecode.prolog_cafe.lang.SymbolTerm;
import com.googlecode.prolog_cafe.lang.Term;
import com.googlecode.prolog_cafe.lang.VariableTerm;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Evaluates a submit-like Prolog rule found in the rules.pl file of the current project and filters
* the results through rules found in the parent projects, all the way up to All-Projects.
*/
public class PrologRuleEvaluator {
private static final Logger log = LoggerFactory.getLogger(PrologRuleEvaluator.class);
public interface Factory {
/** Returns a new {@link PrologRuleEvaluator} with the specified options */
PrologRuleEvaluator create(ChangeData cd, SubmitRuleOptions options);
}
/**
* Exception thrown when the label term of a submit record unexpectedly didn't contain a user
* term.
*/
private static class UserTermExpected extends Exception {
private static final long serialVersionUID = 1L;
UserTermExpected(SubmitRecord.Label label) {
super(String.format("A label with the status %s must contain a user.", label.toString()));
}
}
private final AccountCache accountCache;
private final Accounts accounts;
private final Emails emails;
private final ChangeData cd;
private final ProjectState projectState;
private final SubmitRuleOptions opts;
private Term submitRule;
@AssistedInject
private PrologRuleEvaluator(
AccountCache accountCache,
Accounts accounts,
Emails emails,
ProjectCache projectCache,
@Assisted ChangeData cd,
@Assisted SubmitRuleOptions options) {
this.accountCache = accountCache;
this.accounts = accounts;
this.emails = emails;
this.cd = cd;
this.opts = options;
this.projectState = projectCache.get(cd.project());
}
private static Term toListTerm(List<Term> terms) {
Term list = Prolog.Nil;
for (int i = terms.size() - 1; i >= 0; i--) {
list = new ListTerm(terms.get(i), list);
}
return list;
}
private static boolean isUser(Term who) {
return who instanceof StructureTerm
&& who.arity() == 1
&& who.name().equals("user")
&& who.arg(0) instanceof IntegerTerm;
}
private Term getSubmitRule() {
return submitRule;
}
/**
* Evaluate the submit rules.
*
* @return List of {@link SubmitRecord} objects returned from the evaluated rules, including any
* errors.
*/
public Collection<SubmitRecord> evaluate() {
Change change;
try {
change = cd.change();
if (change == null) {
throw new OrmException("No change found");
}
if (projectState == null) {
throw new NoSuchProjectException(cd.project());
}
} catch (OrmException | NoSuchProjectException e) {
return ruleError("Error looking up change " + cd.getId(), e);
}
if (!opts.allowClosed() && change.getStatus().isClosed()) {
SubmitRecord rec = new SubmitRecord();
rec.status = SubmitRecord.Status.CLOSED;
return Collections.singletonList(rec);
}
List<Term> results;
try {
results =
evaluateImpl(
"locate_submit_rule", "can_submit", "locate_submit_filter", "filter_submit_results");
} catch (RuleEvalException e) {
return ruleError(e.getMessage(), e);
}
if (results.isEmpty()) {
// This should never occur. A well written submit rule will always produce
// at least one result informing the caller of the labels that are
// required for this change to be submittable. Each label will indicate
// whether or not that is actually possible given the permissions.
return ruleError(
String.format(
"Submit rule '%s' for change %s of %s has no solution.",
getSubmitRuleName(), cd.getId(), projectState.getName()));
}
return resultsToSubmitRecord(getSubmitRule(), results);
}
private String getSubmitRuleName() {
return submitRule == null ? "<unknown>" : submitRule.name();
}
/**
* Convert the results from Prolog Cafe's format to Gerrit's common format.
*
* <p>can_submit/1 terminates when an ok(P) record is found. Therefore walk the results backwards,
* using only that ok(P) record if it exists. This skips partial results that occur early in the
* output. Later after the loop the out collection is reversed to restore it to the original
* ordering.
*/
public List<SubmitRecord> resultsToSubmitRecord(Term submitRule, List<Term> results) {
boolean foundOk = false;
List<SubmitRecord> out = new ArrayList<>(results.size());
for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
Term submitRecord = results.get(resultIdx);
SubmitRecord rec = new SubmitRecord();
out.add(rec);
if (!(submitRecord instanceof StructureTerm) || 1 != submitRecord.arity()) {
return invalidResult(submitRule, submitRecord);
}
if ("ok".equals(submitRecord.name())) {
rec.status = SubmitRecord.Status.OK;
} else if ("not_ready".equals(submitRecord.name())) {
rec.status = SubmitRecord.Status.NOT_READY;
} else {
return invalidResult(submitRule, submitRecord);
}
// Unpack the one argument. This should also be a structure with one
// argument per label that needs to be reported on to the caller.
//
submitRecord = submitRecord.arg(0);
if (!(submitRecord instanceof StructureTerm)) {
return invalidResult(submitRule, submitRecord);
}
rec.labels = new ArrayList<>(submitRecord.arity());
for (Term state : ((StructureTerm) submitRecord).args()) {
if (!(state instanceof StructureTerm)
|| 2 != state.arity()
|| !"label".equals(state.name())) {
return invalidResult(submitRule, submitRecord);
}
SubmitRecord.Label lbl = new SubmitRecord.Label();
rec.labels.add(lbl);
lbl.label = state.arg(0).name();
Term status = state.arg(1);
try {
if ("ok".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.OK;
appliedBy(lbl, status);
} else if ("reject".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.REJECT;
appliedBy(lbl, status);
} else if ("need".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.NEED;
} else if ("may".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.MAY;
} else if ("impossible".equals(status.name())) {
lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
} else {
return invalidResult(submitRule, submitRecord);
}
} catch (UserTermExpected e) {
return invalidResult(submitRule, submitRecord, e.getMessage());
}
}
if (rec.status == SubmitRecord.Status.OK) {
foundOk = true;
break;
}
}
Collections.reverse(out);
// This transformation is required to adapt Prolog's behavior to the way Gerrit handles
// SubmitRecords, as defined in the SubmitRecord#allRecordsOK method.
// When several rules are defined in Prolog, they are all matched to a SubmitRecord. We want
// the change to be submittable when at least one result is OK.
if (foundOk) {
for (SubmitRecord record : out) {
record.status = SubmitRecord.Status.OK;
}
}
return out;
}
private List<SubmitRecord> invalidResult(Term rule, Term record, String reason) {
return ruleError(
String.format(
"Submit rule %s for change %s of %s output invalid result: %s%s",
rule,
cd.getId(),
cd.project().get(),
record,
(reason == null ? "" : ". Reason: " + reason)));
}
private List<SubmitRecord> invalidResult(Term rule, Term record) {
return invalidResult(rule, record, null);
}
private List<SubmitRecord> ruleError(String err) {
return ruleError(err, null);
}
private List<SubmitRecord> ruleError(String err, Exception e) {
if (opts.logErrors()) {
if (e == null) {
log.error(err);
} else {
log.error(err, e);
}
return defaultRuleError();
}
return createRuleError(err);
}
/**
* Evaluate the submit type rules to get the submit type.
*
* @return record from the evaluated rules.
*/
public SubmitTypeRecord getSubmitType() {
try {
if (projectState == null) {
throw new NoSuchProjectException(cd.project());
}
} catch (NoSuchProjectException e) {
return typeError("Error looking up change " + cd.getId(), e);
}
List<Term> results;
try {
results =
evaluateImpl(
"locate_submit_type",
"get_submit_type",
"locate_submit_type_filter",
"filter_submit_type_results");
} catch (RuleEvalException e) {
return typeError(e.getMessage(), e);
}
if (results.isEmpty()) {
// Should never occur for a well written rule
return typeError(
"Submit rule '"
+ getSubmitRuleName()
+ "' for change "
+ cd.getId()
+ " of "
+ projectState.getName()
+ " has no solution.");
}
Term typeTerm = results.get(0);
if (!(typeTerm instanceof SymbolTerm)) {
return typeError(
"Submit rule '"
+ getSubmitRuleName()
+ "' for change "
+ cd.getId()
+ " of "
+ projectState.getName()
+ " did not return a symbol.");
}
String typeName = typeTerm.name();
try {
return SubmitTypeRecord.OK(SubmitType.valueOf(typeName.toUpperCase()));
} catch (IllegalArgumentException e) {
return typeError(
"Submit type rule "
+ getSubmitRule()
+ " for change "
+ cd.getId()
+ " of "
+ projectState.getName()
+ " output invalid result: "
+ typeName);
}
}
private SubmitTypeRecord typeError(String err) {
return typeError(err, null);
}
private SubmitTypeRecord typeError(String err, Exception e) {
if (opts.logErrors()) {
if (e == null) {
log.error(err);
} else {
log.error(err, e);
}
return defaultTypeError();
}
return SubmitTypeRecord.error(err);
}
private List<Term> evaluateImpl(
String userRuleLocatorName,
String userRuleWrapperName,
String filterRuleLocatorName,
String filterRuleWrapperName)
throws RuleEvalException {
PrologEnvironment env = getPrologEnvironment();
try {
Term sr = env.once("gerrit", userRuleLocatorName, new VariableTerm());
List<Term> results = new ArrayList<>();
try {
for (Term[] template : env.all("gerrit", userRuleWrapperName, sr, new VariableTerm())) {
results.add(template[1]);
}
} catch (ReductionLimitException err) {
throw new RuleEvalException(
String.format(
"%s on change %d of %s",
err.getMessage(), cd.getId().get(), projectState.getName()));
} catch (RuntimeException err) {
throw new RuleEvalException(
String.format(
"Exception calling %s on change %d of %s",
sr, cd.getId().get(), projectState.getName()),
err);
}
Term resultsTerm = toListTerm(results);
if (!opts.skipFilters()) {
resultsTerm =
runSubmitFilters(resultsTerm, env, filterRuleLocatorName, filterRuleWrapperName);
}
List<Term> r;
if (resultsTerm instanceof ListTerm) {
r = new ArrayList<>();
for (Term t = resultsTerm; t instanceof ListTerm; ) {
ListTerm l = (ListTerm) t;
r.add(l.car().dereference());
t = l.cdr().dereference();
}
} else {
r = Collections.emptyList();
}
submitRule = sr;
return r;
} finally {
env.close();
}
}
private PrologEnvironment getPrologEnvironment() throws RuleEvalException {
PrologEnvironment env;
try {
if (opts.rule() == null) {
env = projectState.newPrologEnvironment();
} else {
env = projectState.newPrologEnvironment("stdin", new StringReader(opts.rule()));
}
} catch (CompileException err) {
String msg;
if (opts.rule() == null) {
msg =
String.format(
"Cannot load rules.pl for %s: %s", projectState.getName(), err.getMessage());
} else {
msg = err.getMessage();
}
throw new RuleEvalException(msg, err);
}
env.set(StoredValues.ACCOUNTS, accounts);
env.set(StoredValues.ACCOUNT_CACHE, accountCache);
env.set(StoredValues.EMAILS, emails);
env.set(StoredValues.REVIEW_DB, cd.db());
env.set(StoredValues.CHANGE_DATA, cd);
env.set(StoredValues.PROJECT_STATE, projectState);
return env;
}
private Term runSubmitFilters(
Term results,
PrologEnvironment env,
String filterRuleLocatorName,
String filterRuleWrapperName)
throws RuleEvalException {
PrologEnvironment childEnv = env;
ChangeData cd = env.get(StoredValues.CHANGE_DATA);
ProjectState projectState = env.get(StoredValues.PROJECT_STATE);
for (ProjectState parentState : projectState.parents()) {
PrologEnvironment parentEnv;
try {
parentEnv = parentState.newPrologEnvironment();
} catch (CompileException err) {
throw new RuleEvalException("Cannot consult rules.pl for " + parentState.getName(), err);
}
parentEnv.copyStoredValues(childEnv);
Term filterRule = parentEnv.once("gerrit", filterRuleLocatorName, new VariableTerm());
try {
Term[] template =
parentEnv.once(
"gerrit", filterRuleWrapperName, filterRule, results, new VariableTerm());
results = template[2];
} catch (ReductionLimitException err) {
throw new RuleEvalException(
String.format(
"%s on change %d of %s",
err.getMessage(), cd.getId().get(), parentState.getName()));
} catch (RuntimeException err) {
throw new RuleEvalException(
String.format(
"Exception calling %s on change %d of %s",
filterRule, cd.getId().get(), parentState.getName()),
err);
}
childEnv = parentEnv;
}
return results;
}
private void appliedBy(SubmitRecord.Label label, Term status) throws UserTermExpected {
if (status instanceof StructureTerm && status.arity() == 1) {
Term who = status.arg(0);
if (isUser(who)) {
label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue());
} else {
throw new UserTermExpected(label);
}
}
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (C) 2018 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.rules;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.extensions.annotations.ExtensionPoint;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import java.util.Collection;
/**
* Allows plugins to decide whether a change is ready to be submitted or not.
*
* <p>For a given {@link ChangeData}, each plugin is called and returns a {@link Collection} of
* {@link SubmitRecord}. This collection can be empty, or contain one or several values.
*
* <p>A Change can only be submitted if all the plugins give their consent.
*
* <p>Each {@link SubmitRecord} represents a decision made by the plugin. If the plugin rejects a
* change, it should hold valuable informations to help the end user understand and correct the
* blocking points.
*
* <p>It should be noted that each plugin can handle rules inheritance.
*
* <p>This interface should be used to write pre-submit validation rules. This includes both simple
* checks, coded in Java, and more complex fully fledged expression evaluators (think: Prolog,
* JavaCC, or even JavaScript rules).
*/
@ExtensionPoint
public interface SubmitRule {
/** Returns a {@link Collection} of {@link SubmitRecord} status for the change. */
Collection<SubmitRecord> evaluate(ChangeData changeData, SubmitRuleOptions options);
}