Store SubmitRecords in index

We can quickly evaluate labels and the submittable bit in ChangeJson
if we store the full submit records in the index. We would also like
to be able to search for submittable changes, i.e. changes that pass
the submit rule evaluator. Support this with a triumvirate of new
fields.

We now support `is:submittable` and `submit:STATUS` operators, as well
as a new flavor of `label:` that takes a submit record label status
rather than a numeric vote. These allow for more precise queries than
some of the heuristics that were previously documented in the search
docs.

Change-Id: Ie8a185a7cdae998be168900186fb64905246e7cf
This commit is contained in:
Dave Borowitz 2016-09-22 16:11:56 +02:00
parent 7de992f7dc
commit 6453fcef85
16 changed files with 555 additions and 43 deletions

View File

@ -5862,6 +5862,8 @@ If not set, the default is `ALL`.
[[submit-record]]
=== SubmitRecord
The `SubmitRecord` entity describes results from a submit_rule.
Fields in this entity roughly correspond to the fields set by `LABELS`
in link:#label-info[LabelInfo].
[options="header",cols="1,^1,5"]
|===========================

View File

@ -321,6 +321,20 @@ is:merged, is:abandoned::
+
Same as <<status,status:'STATE'>>.
is:submittable::
+
True if the change is submittable according to the submit rules for
the project, for example if all necessary labels have been voted on.
+
This operator only takes into account one change at a time, not any
related changes, and does not guarantee that the submit button will
appear for matching changes. To check whether a submit button appears,
use the
link:rest-api-changes.html#get-revision-actions[Get Revision Actions]
API.
+
Equivalent to <<submittable,submittable:ok>>.
[[mergeable]]
is:mergeable::
+
@ -394,6 +408,15 @@ Changes where 'COMMITTER' is the committer of the current patch set.
'COMMITTER' may be the committer's exact email address, or part of the name or
email address.
[[submittable]]
submittable:'SUBMIT_STATUS'::
+
Changes having the given submit record status after applying submit
rules. Valid statuses are in the `status` field of
link:rest-api-changes.html#submit-record[SubmitRecord]. This operator
only applies to the top-level status; individual label statuses can be
searched link:#labels[by label].
== Argument Quoting
@ -448,8 +471,10 @@ A label name is any of the following:
('user=' or 'group='). If an LDAP group is being referenced make
sure to use 'ldap/<groupname>'.
A label name must be followed by a score, or an operator and a score.
The easiest way to explain this is by example.
A label name must be followed by either a score with optional operator,
or a label status. The easiest way to explain this is by example.
+
First, some examples of scores with operators:
`label:Code-Review=2`::
`label:Code-Review=+2`::
@ -473,8 +498,20 @@ Scores of +2 are not matched, even though they are higher.
`label:Code-Review>=1`::
+
Matches changes with either a +1, +2, or any higher score.
+
Instead of a numeric vote, you can provide a label status corresponding
to one of the fields in the
link:rest-api-changes.html#submit-record[SubmitRecord] REST API entity.
`label:Non-Author-Code-Review=need`::
+
Matches changes where the submit rules indicate that a label named
`Non-Author-Code-Review` is needed. (See the
link:prolog-cookbook.html#NonAuthorCodeReview[Prolog Cookbook] for how
this label can be configured.)
`label:Code-Review=+2,aname`::
`label:Code-Review=ok,aname`::
+
Matches changes with a +2 code review where the reviewer or group is aname.
@ -483,7 +520,9 @@ Matches changes with a +2 code review where the reviewer or group is aname.
Matches changes with a +2 code review where the reviewer is jsmith.
`label:Code-Review=+2,user=owner`::
`label:Code-Review=ok,user=owner`::
`label:Code-Review=+2,owner`::
`label:Code-Review=ok,owner`::
+
The special "owner" parameter corresponds to the change owner. Matches
all changes that have a +2 vote from the change owner.
@ -498,10 +537,14 @@ ldap/linux.workflow group.
Matches changes with either a -1, -2, or any lower score.
`is:open label:Code-Review+2 label:Verified+1 NOT label:Verified-1 NOT label:Code-Review-2`::
`is:open label:Code-Review=ok label:Verified=ok`::
+
Matches changes that are ready to be submitted.
Matches changes that are ready to be submitted according to one common
label configuration. (For a more general check, use
link:#submittable[submittable:ok].)
`is:open (label:Verified-1 OR label:Code-Review-2)`::
`is:open (label:Verified=reject OR label:Code-Review:reject)`::
+
Changes that are blocked from submission due to a blocking score.

View File

@ -16,14 +16,27 @@ package com.google.gerrit.common.data;
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.
*/
public class SubmitRecord {
public static Optional<SubmitRecord> findOkRecord(
Collection<SubmitRecord> in) {
if (in == null) {
return Optional.empty();
}
return in.stream().filter(r -> r.status == Status.OK).findFirst();
}
public enum Status {
// NOTE: These values are persisted in the index, so deleting or changing
// the name of any values requires a schema upgrade.
/** The change is ready for submission. */
OK,
@ -50,6 +63,9 @@ public class SubmitRecord {
public static class Label {
public enum Status {
// NOTE: These values are persisted in the index, so deleting or changing
// the name of any values requires a schema upgrade.
/**
* This label provides what is necessary for submission.
* <p>

View File

@ -17,6 +17,8 @@ package com.google.gerrit.elasticsearch;
import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
import static com.google.gson.FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.commons.codec.binary.Base64.decodeBase64;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
@ -44,6 +46,7 @@ import com.google.gerrit.server.index.change.ChangeField.PatchSetApprovalProtoFi
import com.google.gerrit.server.index.change.ChangeField.PatchSetProtoField;
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.change.ChangeIndexRewriter;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeData;
@ -148,7 +151,7 @@ class ElasticChangeIndex extends AbstractElasticIndex<Change.Id, ChangeData>
private static <T> List<T> decodeProtos(JsonObject doc, String fieldName,
ProtobufCodec<T> codec) {
return FluentIterable.from(doc.getAsJsonArray(fieldName))
.transform(i -> codec.decode(Base64.decodeBase64(i.toString())))
.transform(i -> codec.decode(decodeBase64(i.toString())))
.toList();
}
@ -383,7 +386,27 @@ class ElasticChangeIndex extends AbstractElasticIndex<Change.Id, ChangeData>
cd.setReviewers(ReviewerSet.empty());
}
decodeSubmitRecords(source,
ChangeField.STORED_SUBMIT_RECORD_STRICT.getName(),
ChangeField.SUBMIT_RULE_OPTIONS_STRICT, cd);
decodeSubmitRecords(source,
ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName(),
ChangeField.SUBMIT_RULE_OPTIONS_LENIENT, cd);
return cd;
}
private void decodeSubmitRecords(JsonObject doc, String fieldName,
SubmitRuleOptions opts, ChangeData out) {
JsonArray records = doc.getAsJsonArray(fieldName);
if (records == null) {
return;
}
ChangeField.parseSubmitRecords(
FluentIterable.from(records)
.transform(i -> new String(decodeBase64(i.toString()), UTF_8))
.toList(),
opts, out);
}
}
}

View File

@ -25,6 +25,7 @@ import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STA
import com.google.common.base.Throwables;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Collections2;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@ -52,6 +53,7 @@ import com.google.gerrit.server.index.change.ChangeField.PatchSetApprovalProtoFi
import com.google.gerrit.server.index.change.ChangeField.PatchSetProtoField;
import com.google.gerrit.server.index.change.ChangeIndex;
import com.google.gerrit.server.index.change.ChangeIndexRewriter;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeData;
@ -127,6 +129,10 @@ public class LuceneChangeIndex implements ChangeIndex {
private static final String HASHTAG_FIELD =
ChangeField.HASHTAG_CASE_AWARE.getName();
private static final String STAR_FIELD = ChangeField.STAR.getName();
private static final String SUBMIT_RECORD_LENIENT_FIELD =
ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName();
private static final String SUBMIT_RECORD_STRICT_FIELD =
ChangeField.STORED_SUBMIT_RECORD_STRICT.getName();
static Term idTerm(ChangeData cd) {
return QueryBuilder.intTerm(LEGACY_ID.getName(), cd.getId().get());
@ -465,6 +471,10 @@ public class LuceneChangeIndex implements ChangeIndex {
if (fields.contains(REVIEWER_FIELD)) {
decodeReviewers(doc, cd);
}
decodeSubmitRecords(doc, SUBMIT_RECORD_STRICT_FIELD,
ChangeField.SUBMIT_RULE_OPTIONS_STRICT, cd);
decodeSubmitRecords(doc, SUBMIT_RECORD_LENIENT_FIELD,
ChangeField.SUBMIT_RULE_OPTIONS_LENIENT, cd);
return cd;
}
@ -557,6 +567,14 @@ public class LuceneChangeIndex implements ChangeIndex {
.transform(IndexableField::stringValue)));
}
private void decodeSubmitRecords(Multimap<String, IndexableField> doc,
String field, SubmitRuleOptions opts, ChangeData cd) {
ChangeField.parseSubmitRecords(
Collections2.transform(
doc.get(field), f -> f.binaryValue().utf8ToString()),
opts, cd);
}
private static <T> List<T> decodeProtos(Multimap<String, IndexableField> doc,
String fieldName, ProtobufCodec<T> codec) {
Collection<IndexableField> fields = doc.get(fieldName);

View File

@ -109,9 +109,8 @@ import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.patch.PatchListNotAvailableException;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.QueryResult;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
@ -144,6 +143,22 @@ import java.util.TreeMap;
public class ChangeJson {
private static final Logger log = LoggerFactory.getLogger(ChangeJson.class);
// Submit rule options in this class should always use fastEvalLabels for
// efficiency reasons. Callers that care about submittability after taking
// vote squashing into account should be looking at the submit action.
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT =
ChangeField.SUBMIT_RULE_OPTIONS_LENIENT
.toBuilder()
.fastEvalLabels(true)
.build();
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
ChangeField.SUBMIT_RULE_OPTIONS_STRICT
.toBuilder()
.fastEvalLabels(true)
.build();
public static final Set<ListChangesOption> NO_OPTIONS =
Collections.emptySet();
@ -180,7 +195,6 @@ public class ChangeJson {
private boolean lazyLoad = true;
private AccountLoader accountLoader;
private Map<Change.Id, List<SubmitRecord>> submitRecords;
private FixInput fix;
@AssistedInject
@ -556,25 +570,13 @@ public class ChangeJson {
}
private boolean submittable(ChangeData cd) throws OrmException {
List<SubmitRecord> records = new SubmitRuleEvaluator(cd)
.setFastEvalLabels(true)
.evaluate();
for (SubmitRecord sr : records) {
if (sr.status == SubmitRecord.Status.OK) {
return true;
}
}
return false;
return SubmitRecord.findOkRecord(
cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT))
.isPresent();
}
private static final SubmitRuleOptions SUBMIT_RULE_OPTIONS =
SubmitRuleOptions.defaults()
.fastEvalLabels(true)
.allowDraft(true)
.build();
private List<SubmitRecord> submitRecords(ChangeData cd) throws OrmException {
return cd.submitRecords(SUBMIT_RULE_OPTIONS);
return cd.submitRecords(SUBMIT_RULE_OPTIONS_LENIENT);
}
private Map<String, LabelInfo> labelsFor(ChangeControl ctl,

View File

@ -81,7 +81,6 @@ import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
@ -255,15 +254,6 @@ public class MergeOp implements AutoCloseable {
orm.close();
}
private static Optional<SubmitRecord> findOkRecord(
Collection<SubmitRecord> in) {
if (in == null) {
return Optional.empty();
}
return in.stream().filter(r -> r.status == SubmitRecord.Status.OK)
.findAny();
}
public static void checkSubmitRule(ChangeData cd)
throws ResourceConflictException, OrmException {
PatchSet patchSet = cd.currentPatchSet();
@ -272,7 +262,7 @@ public class MergeOp implements AutoCloseable {
"missing current patch set for change " + cd.getId());
}
List<SubmitRecord> results = getSubmitRecords(cd);
if (findOkRecord(results).isPresent()) {
if (SubmitRecord.findOkRecord(results).isPresent()) {
// Rules supplied a valid solution.
return;
} else if (results.isEmpty()) {

View File

@ -15,7 +15,9 @@
package com.google.gerrit.server.index.change;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import com.google.common.annotations.VisibleForTesting;
@ -27,21 +29,25 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.collect.Table;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Comment;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.server.OutputFormat;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.index.FieldDef;
import com.google.gerrit.server.index.FieldType;
import com.google.gerrit.server.index.SchemaUtil;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeStatusPredicate;
import com.google.gson.Gson;
import com.google.gwtorm.protobuf.CodecFactory;
import com.google.gwtorm.protobuf.ProtobufCodec;
import com.google.gwtorm.server.OrmException;
@ -74,6 +80,8 @@ import java.util.Set;
public class ChangeField {
public static final int NO_ASSIGNEE = -1;
private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
/** Legacy change ID. */
public static final FieldDef<ChangeData, Integer> LEGACY_ID =
new FieldDef.Single<ChangeData, Integer>("legacy_id",
@ -774,6 +782,169 @@ public class ChangeField {
}
};
// Submit rule options in this class should never use fastEvalLabels. This
// slows down indexing slightly but produces correct search results.
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT =
SubmitRuleOptions.defaults()
.allowClosed(true)
.allowDraft(true)
.build();
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
SubmitRuleOptions.defaults().build();
/**
* JSON type for storing SubmitRecords.
* <p>
* Stored fields need to use a stable format over a long period; this type
* insulates the index from implementation changes in SubmitRecord itself.
*/
static class StoredSubmitRecord {
static class StoredLabel {
String label;
SubmitRecord.Label.Status status;
Integer appliedBy;
}
SubmitRecord.Status status;
List<StoredLabel> labels;
String errorMessage;
StoredSubmitRecord(SubmitRecord rec) {
this.status = rec.status;
this.errorMessage = rec.errorMessage;
if (rec.labels != null) {
this.labels = new ArrayList<>(rec.labels.size());
for (SubmitRecord.Label label : rec.labels) {
StoredLabel sl = new StoredLabel();
sl.label = label.label;
sl.status = label.status;
sl.appliedBy =
label.appliedBy != null ? label.appliedBy.get() : null;
this.labels.add(sl);
}
}
}
private SubmitRecord toSubmitRecord() {
SubmitRecord rec = new SubmitRecord();
rec.status = status;
rec.errorMessage = errorMessage;
if (labels != null) {
rec.labels = new ArrayList<>(labels.size());
for (StoredLabel label : labels) {
SubmitRecord.Label srl = new SubmitRecord.Label();
srl.label = label.label;
srl.status = label.status;
srl.appliedBy = label.appliedBy != null
? new Account.Id(label.appliedBy)
: null;
rec.labels.add(srl);
}
}
return rec;
}
}
public static final FieldDef<ChangeData, Iterable<String>> SUBMIT_RECORD =
new FieldDef.Repeatable<ChangeData, String>(
"submit_record", FieldType.EXACT, false) {
@Override
public Iterable<String> get(ChangeData input, FillArgs args)
throws OrmException {
return formatSubmitRecordValues(input);
}
};
public static final FieldDef<ChangeData, Iterable<byte[]>>
STORED_SUBMIT_RECORD_STRICT =
new FieldDef.Repeatable<ChangeData, byte[]>(
"full_submit_record_strict", FieldType.STORED_ONLY, true) {
@Override
public Iterable<byte[]> get(ChangeData input, FillArgs args)
throws OrmException {
return storedSubmitRecords(input, SUBMIT_RULE_OPTIONS_STRICT);
}
};
public static final FieldDef<ChangeData, Iterable<byte[]>>
STORED_SUBMIT_RECORD_LENIENT =
new FieldDef.Repeatable<ChangeData, byte[]>(
"full_submit_record_lenient", FieldType.STORED_ONLY, true) {
@Override
public Iterable<byte[]> get(ChangeData input, FillArgs args)
throws OrmException {
return storedSubmitRecords(input, SUBMIT_RULE_OPTIONS_LENIENT);
}
};
public static void parseSubmitRecords(
Collection<String> values, SubmitRuleOptions opts, ChangeData out) {
checkArgument(!opts.fastEvalLabels());
List<SubmitRecord> records = parseSubmitRecords(values);
if (records.isEmpty()) {
// Assume no values means the field is not in the index;
// SubmitRuleEvaluator ensures the list is non-empty.
return;
}
out.setSubmitRecords(opts, records);
// Cache the fastEvalLabels variant as well so it can be used by
// ChangeJson.
out.setSubmitRecords(
opts.toBuilder().fastEvalLabels(true).build(),
records);
}
@VisibleForTesting
static List<SubmitRecord> parseSubmitRecords(Collection<String> values) {
return values.stream()
.map(v -> GSON.fromJson(v, StoredSubmitRecord.class).toSubmitRecord())
.collect(toList());
}
@VisibleForTesting
static List<byte[]> storedSubmitRecords(List<SubmitRecord> records) {
return Lists.transform(
records, r -> GSON.toJson(new StoredSubmitRecord(r)).getBytes(UTF_8));
}
private static Iterable<byte[]> storedSubmitRecords(
ChangeData cd, SubmitRuleOptions opts) throws OrmException {
return storedSubmitRecords(cd.submitRecords(opts));
}
public static List<String> formatSubmitRecordValues(ChangeData cd)
throws OrmException {
return formatSubmitRecordValues(
cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT),
cd.change().getOwner());
}
@VisibleForTesting
static List<String> formatSubmitRecordValues(List<SubmitRecord> records,
Account.Id changeOwner) {
List<String> result = new ArrayList<>();
for (SubmitRecord rec : records) {
result.add(rec.status.name());
if (rec.labels == null) {
continue;
}
for (SubmitRecord.Label label : rec.labels) {
String sl = label.status.toString() + ',' + label.label.toLowerCase();
result.add(sl);
String slc = sl + ',';
if (label.appliedBy != null) {
result.add(slc + label.appliedBy.get());
if (label.appliedBy.equals(changeOwner)) {
result.add(slc + ChangeQueryBuilder.OWNER_ACCOUNT_ID.get());
}
}
}
}
return result;
}
public static final Integer NOT_REVIEWED = -1;
private static String getTopic(ChangeData input) throws OrmException {

View File

@ -66,13 +66,19 @@ public class ChangeSchemaDefinitions extends SchemaDefinitions<ChangeData> {
static final Schema<ChangeData> V33 =
schema(V32, ChangeField.ASSIGNEE);
@SuppressWarnings("deprecation")
@Deprecated
static final Schema<ChangeData> V34 = new Schema.Builder<ChangeData>()
.add(V33)
.remove(ChangeField.LABEL)
.add(ChangeField.LABEL2)
.build();
static final Schema<ChangeData> V35 =
schema(V34,
ChangeField.SUBMIT_RECORD,
ChangeField.STORED_SUBMIT_RECORD_LENIENT,
ChangeField.STORED_SUBMIT_RECORD_STRICT);
public static final String NAME = "changes";
public static final ChangeSchemaDefinitions INSTANCE =
new ChangeSchemaDefinitions();

View File

@ -120,12 +120,7 @@ public class SubmitRuleEvaluator {
public SubmitRuleEvaluator setOptions(SubmitRuleOptions opts) {
checkNotStarted();
if (opts != null) {
optsBuilder = SubmitRuleOptions.builder()
.fastEvalLabels(opts.fastEvalLabels())
.allowDraft(opts.allowDraft())
.allowClosed(opts.allowClosed())
.skipFilters(opts.skipFilters())
.rule(opts.rule());
optsBuilder = opts.toBuilder();
} else {
optsBuilder = SubmitRuleOptions.defaults();
}
@ -273,7 +268,10 @@ public class SubmitRuleEvaluator {
}
return createRuleError("Cannot submit draft changes");
} catch (OrmException err) {
String msg = "Cannot check visibility of patch set " + patchSet.getId();
PatchSet.Id psId = patchSet != null
? patchSet.getId()
: control.getChange().currentPatchSetId();
String msg = "Cannot check visibility of patch set " + psId;
log.error(msg, err);
return createRuleError(msg);
}

View File

@ -55,4 +55,13 @@ public abstract class SubmitRuleOptions {
public abstract SubmitRuleOptions build();
}
public Builder toBuilder() {
return builder()
.fastEvalLabels(fastEvalLabels())
.allowDraft(allowDraft())
.allowClosed(allowClosed())
.skipFilters(skipFilters())
.rule(rule());
}
}

View File

@ -19,11 +19,13 @@ import static com.google.gerrit.server.query.change.ChangeData.asChanges;
import static java.util.stream.Collectors.toSet;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Enums;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.common.errors.NotSignedInException;
import com.google.gerrit.extensions.registration.DynamicMap;
import com.google.gerrit.reviewdb.client.Account;
@ -488,6 +490,10 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
return new AssigneePredicate(new Account.Id(ChangeField.NO_ASSIGNEE));
}
if ("submittable".equalsIgnoreCase(value)) {
return new SubmittablePredicate(SubmitRecord.Status.OK);
}
try {
return status(value);
} catch (IllegalArgumentException e) {
@ -644,7 +650,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
try {
group = parseGroup(value).getUUID();
} catch (QueryParseException e) {
throw error("Neither user nor group " + value + " found");
throw error("Neither user nor group " + value + " found", e);
}
}
}
@ -665,9 +671,35 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
}
}
// If the vote piece looks like Code-Review=NEED with a valid non-numeric
// submit record status, interpret as a submit record query.
int eq = name.indexOf('=');
if (args.getSchema().hasField(ChangeField.SUBMIT_RECORD) && eq > 0) {
String statusName = name.substring(eq + 1).toUpperCase();
if (!isInt(statusName)) {
SubmitRecord.Label.Status status = Enums.getIfPresent(
SubmitRecord.Label.Status.class, statusName).orNull();
if (status == null) {
throw error("Invalid label status " + statusName + " in " + name);
}
return SubmitRecordPredicate.create(
name.substring(0, eq), status, accounts);
}
}
return new LabelPredicate(args, name, accounts, group);
}
private static boolean isInt(String s) {
if (s == null) {
return false;
}
if (s.startsWith("+")) {
s = s.substring(1);
}
return Ints.tryParse(s) != null;
}
@Operator
public Predicate<ChangeData> message(String text) {
return new MessagePredicate(args.index, text);
@ -964,6 +996,17 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
return new CommitterPredicate(who);
}
@Operator
public Predicate<ChangeData> submittable(String str)
throws QueryParseException {
SubmitRecord.Status status = Enums.getIfPresent(
SubmitRecord.Status.class, str.toUpperCase()).orNull();
if (status == null) {
throw error("invalid value for submittable:" + str);
}
return new SubmittablePredicate(status);
}
@Override
protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
if (query.startsWith("refs/")) {

View File

@ -0,0 +1,54 @@
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.query.change;
import static java.util.stream.Collectors.toList;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.query.Predicate;
import com.google.gwtorm.server.OrmException;
import java.util.Set;
class SubmitRecordPredicate extends ChangeIndexPredicate {
static Predicate<ChangeData> create(String label,
SubmitRecord.Label.Status status, Set<Account.Id> accounts) {
String lowerLabel = label.toLowerCase();
if (accounts == null || accounts.isEmpty()) {
return new SubmitRecordPredicate(status.name() + ',' + lowerLabel);
}
return Predicate.or(
accounts.stream()
.map(a -> new SubmitRecordPredicate(
status.name() + ',' + lowerLabel + ',' + a.get()))
.collect(toList()));
}
private SubmitRecordPredicate(String value) {
super(ChangeField.SUBMIT_RECORD, value);
}
@Override
public boolean match(ChangeData in) throws OrmException {
return ChangeField.formatSubmitRecordValues(in).contains(getValue());
}
@Override
public int getCost() {
return 1;
}
}

View File

@ -0,0 +1,39 @@
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.query.change;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gwtorm.server.OrmException;
class SubmittablePredicate extends ChangeIndexPredicate {
private final SubmitRecord.Status status;
SubmittablePredicate(SubmitRecord.Status status) {
super(ChangeField.SUBMIT_RECORD, status.name());
this.status = status;
}
@Override
public boolean match(ChangeData cd) throws OrmException {
return cd.submitRecords(ChangeField.SUBMIT_RULE_OPTIONS_STRICT).stream()
.anyMatch(r -> r.status == status);
}
@Override
public int getCost() {
return 1;
}
}

View File

@ -15,10 +15,14 @@
package com.google.gerrit.server.index.change;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Table;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.SubmitRecord;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.server.ReviewerSet;
import com.google.gerrit.server.notedb.ReviewerStateInternal;
@ -70,4 +74,63 @@ public class ChangeFieldTest extends GerritBaseTests {
assertThat(ChangeField.parseReviewerFieldValues(values))
.isEqualTo(reviewers);
}
@Test
public void formatSubmitRecordValues() {
assertThat(
ChangeField.formatSubmitRecordValues(
ImmutableList.of(
record(
SubmitRecord.Status.OK,
label(SubmitRecord.Label.Status.MAY, "Label-1", null),
label(SubmitRecord.Label.Status.OK, "Label-2", 1))),
new Account.Id(1)))
.containsExactly(
"OK",
"MAY,label-1",
"OK,label-2",
"OK,label-2,0",
"OK,label-2,1");
}
@Test
public void storedSubmitRecords() {
assertStoredRecordRoundTrip(record(SubmitRecord.Status.CLOSED));
assertStoredRecordRoundTrip(
record(
SubmitRecord.Status.OK,
label(SubmitRecord.Label.Status.MAY, "Label-1", null),
label(SubmitRecord.Label.Status.OK, "Label-2", 1)));
}
private static SubmitRecord record(SubmitRecord.Status status,
SubmitRecord.Label... labels) {
SubmitRecord r = new SubmitRecord();
r.status = status;
if (labels.length > 0) {
r.labels = ImmutableList.copyOf(labels);
}
return r;
}
private static SubmitRecord.Label label(SubmitRecord.Label.Status status,
String label, Integer appliedBy) {
SubmitRecord.Label l = new SubmitRecord.Label();
l.status = status;
l.label = label;
if (appliedBy != null) {
l.appliedBy = new Account.Id(appliedBy);
}
return l;
}
private static void assertStoredRecordRoundTrip(SubmitRecord... records) {
List<SubmitRecord> recordList = ImmutableList.copyOf(records);
List<String> stored = ChangeField.storedSubmitRecords(recordList).stream()
.map(s -> new String(s, UTF_8))
.collect(toList());
assertThat(ChangeField.parseSubmitRecords(stored))
.named("JSON %s" + stored)
.isEqualTo(recordList);
}
}

View File

@ -1441,6 +1441,41 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
assertQuery("reviewer:" + id, change2, change1);
}
@Test
public void submitRecords() throws Exception {
Account.Id user1 = createAccount("user1");
TestRepository<Repo> repo = createProject("repo");
Change change1 = insert(repo, newChange(repo));
Change change2 = insert(repo, newChange(repo));
gApi.changes()
.id(change1.getId().get())
.current()
.review(ReviewInput.approve());
requestContext.setContext(newRequestContext(user1));
gApi.changes()
.id(change2.getId().get())
.current()
.review(ReviewInput.recommend());
requestContext.setContext(newRequestContext(user.getAccountId()));
assertQuery("is:submittable", change1);
assertQuery("-is:submittable", change2);
assertQuery("submittable:ok", change1);
assertQuery("submittable:not_ready", change2);
assertQuery("label:CodE-RevieW=ok", change1);
assertQuery("label:CodE-RevieW=ok,user=user", change1);
assertQuery("label:CodE-RevieW=ok,Administrators", change1);
assertQuery("label:CodE-RevieW=ok,group=Administrators", change1);
assertQuery("label:CodE-RevieW=ok,owner", change1);
assertQuery("label:CodE-RevieW=ok,user1");
assertQuery("label:CodE-RevieW=need", change2);
// NEED records don't have associated users.
assertQuery("label:CodE-RevieW=need,user1");
assertQuery("label:CodE-RevieW=need,user");
}
@Test
public void byCommitsOnBranchNotMerged() throws Exception {
TestRepository<Repo> repo = createProject("repo");