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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
80
java/com/google/gerrit/common/data/SubmitRequirement.java
Normal file
80
java/com/google/gerrit/common/data/SubmitRequirement.java
Normal 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
|
||||
+ '\''
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,6 +728,7 @@ public class ChangeUpdate extends AbstractChangeUpdate {
|
||||
msg.append('\n');
|
||||
}
|
||||
}
|
||||
// TODO(maximeg) We might want to list plugins that validated this submission.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
48
java/com/google/gerrit/server/rules/PrologRule.java
Normal file
48
java/com/google/gerrit/server/rules/PrologRule.java
Normal 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();
|
||||
}
|
||||
}
|
||||
519
java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
Normal file
519
java/com/google/gerrit/server/rules/PrologRuleEvaluator.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
java/com/google/gerrit/server/rules/SubmitRule.java
Normal file
44
java/com/google/gerrit/server/rules/SubmitRule.java
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user