Line data Source code
1 : // Copyright (C) 2013 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.index.change;
16 :
17 : import static com.google.common.base.MoreObjects.firstNonNull;
18 : import static com.google.common.collect.ImmutableList.toImmutableList;
19 : import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
20 : import static com.google.common.collect.ImmutableSet.toImmutableSet;
21 : import static com.google.gerrit.index.FieldDef.exact;
22 : import static com.google.gerrit.index.FieldDef.fullText;
23 : import static com.google.gerrit.index.FieldDef.intRange;
24 : import static com.google.gerrit.index.FieldDef.integer;
25 : import static com.google.gerrit.index.FieldDef.prefix;
26 : import static com.google.gerrit.index.FieldDef.storedOnly;
27 : import static com.google.gerrit.index.FieldDef.timestamp;
28 : import static com.google.gerrit.server.util.AttentionSetUtil.additionsOnly;
29 : import static java.nio.charset.StandardCharsets.UTF_8;
30 : import static java.util.stream.Collectors.joining;
31 : import static java.util.stream.Collectors.toList;
32 : import static java.util.stream.Collectors.toSet;
33 :
34 : import com.google.common.annotations.VisibleForTesting;
35 : import com.google.common.base.Splitter;
36 : import com.google.common.collect.HashBasedTable;
37 : import com.google.common.collect.ImmutableList;
38 : import com.google.common.collect.ImmutableMap;
39 : import com.google.common.collect.ImmutableSet;
40 : import com.google.common.collect.ImmutableTable;
41 : import com.google.common.collect.Iterables;
42 : import com.google.common.collect.Lists;
43 : import com.google.common.collect.Table;
44 : import com.google.common.flogger.FluentLogger;
45 : import com.google.common.io.Files;
46 : import com.google.common.primitives.Longs;
47 : import com.google.gerrit.common.Nullable;
48 : import com.google.gerrit.entities.Account;
49 : import com.google.gerrit.entities.Address;
50 : import com.google.gerrit.entities.AttentionSetUpdate;
51 : import com.google.gerrit.entities.Change;
52 : import com.google.gerrit.entities.ChangeMessage;
53 : import com.google.gerrit.entities.LabelType;
54 : import com.google.gerrit.entities.LegacySubmitRequirement;
55 : import com.google.gerrit.entities.PatchSetApproval;
56 : import com.google.gerrit.entities.Project;
57 : import com.google.gerrit.entities.RefNames;
58 : import com.google.gerrit.entities.SubmitRecord;
59 : import com.google.gerrit.entities.SubmitRequirementResult;
60 : import com.google.gerrit.entities.converter.ChangeProtoConverter;
61 : import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
62 : import com.google.gerrit.entities.converter.PatchSetProtoConverter;
63 : import com.google.gerrit.entities.converter.ProtoConverter;
64 : import com.google.gerrit.index.FieldDef;
65 : import com.google.gerrit.index.IndexedField;
66 : import com.google.gerrit.index.RefState;
67 : import com.google.gerrit.index.SchemaFieldDefs;
68 : import com.google.gerrit.index.SchemaUtil;
69 : import com.google.gerrit.json.OutputFormat;
70 : import com.google.gerrit.proto.Protos;
71 : import com.google.gerrit.server.ReviewerByEmailSet;
72 : import com.google.gerrit.server.ReviewerSet;
73 : import com.google.gerrit.server.StarredChangesUtil;
74 : import com.google.gerrit.server.config.AllUsersName;
75 : import com.google.gerrit.server.index.change.StalenessChecker.RefStatePattern;
76 : import com.google.gerrit.server.notedb.ReviewerStateInternal;
77 : import com.google.gerrit.server.notedb.SubmitRequirementProtoConverter;
78 : import com.google.gerrit.server.project.SubmitRuleOptions;
79 : import com.google.gerrit.server.query.change.ChangeData;
80 : import com.google.gerrit.server.query.change.ChangeQueryBuilder;
81 : import com.google.gerrit.server.query.change.ChangeStatusPredicate;
82 : import com.google.gerrit.server.query.change.MagicLabelValue;
83 : import com.google.gson.Gson;
84 : import com.google.protobuf.MessageLite;
85 : import java.sql.Timestamp;
86 : import java.time.Instant;
87 : import java.util.ArrayList;
88 : import java.util.Arrays;
89 : import java.util.Collection;
90 : import java.util.HashSet;
91 : import java.util.List;
92 : import java.util.Locale;
93 : import java.util.Map;
94 : import java.util.Objects;
95 : import java.util.Optional;
96 : import java.util.Set;
97 : import java.util.function.Function;
98 : import java.util.stream.Collectors;
99 : import java.util.stream.Stream;
100 : import java.util.stream.StreamSupport;
101 : import org.eclipse.jgit.lib.PersonIdent;
102 :
103 : /**
104 : * Fields indexed on change documents.
105 : *
106 : * <p>Each field corresponds to both a field name supported by {@link ChangeQueryBuilder} for
107 : * querying that field, and a method on {@link ChangeData} used for populating the corresponding
108 : * document fields in the secondary index.
109 : *
110 : * <p>Field names are all lowercase alphanumeric plus underscore; index implementations may create
111 : * unambiguous derived field names containing other characters.
112 : *
113 : * <p>Note that this class does not override {@link Object#equals(Object)}. It relies on instances
114 : * being singletons so that the default (i.e. reference) comparison works.
115 : */
116 0 : public class ChangeField {
117 155 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
118 :
119 : public static final int NO_ASSIGNEE = -1;
120 :
121 155 : private static final Gson GSON = OutputFormat.JSON_COMPACT.newGson();
122 :
123 : /**
124 : * To avoid the non-google dependency on org.apache.lucene.index.IndexWriter.MAX_TERM_LENGTH it is
125 : * redefined here.
126 : */
127 : public static final int MAX_TERM_LENGTH = (1 << 15) - 2;
128 :
129 : // TODO: Rename LEGACY_ID to NUMERIC_ID
130 : /** Legacy change ID. */
131 155 : public static final FieldDef<ChangeData, String> LEGACY_ID_STR =
132 155 : exact("legacy_id_str").stored().build(cd -> String.valueOf(cd.getId().get()));
133 :
134 : /** Newer style Change-Id key. */
135 155 : public static final FieldDef<ChangeData, String> ID =
136 155 : prefix(ChangeQueryBuilder.FIELD_CHANGE_ID).build(changeGetter(c -> c.getKey().get()));
137 :
138 : /** Change status string, in the same format as {@code status:}. */
139 155 : public static final IndexedField<ChangeData, String> STATUS_FIELD =
140 155 : IndexedField.<ChangeData>stringBuilder("Status")
141 155 : .required()
142 155 : .size(20)
143 155 : .build(changeGetter(c -> ChangeStatusPredicate.canonicalize(c.getStatus())));
144 :
145 155 : public static final IndexedField<ChangeData, String>.SearchSpec STATUS_SPEC =
146 155 : STATUS_FIELD.exact(ChangeQueryBuilder.FIELD_STATUS);
147 :
148 : /** Project containing the change. */
149 155 : public static final IndexedField<ChangeData, String> PROJECT_FIELD =
150 155 : IndexedField.<ChangeData>stringBuilder("Project")
151 155 : .required()
152 155 : .stored()
153 155 : .size(200)
154 155 : .build(changeGetter(c -> c.getProject().get()));
155 :
156 155 : public static final IndexedField<ChangeData, String>.SearchSpec PROJECT_SPEC =
157 155 : PROJECT_FIELD.exact(ChangeQueryBuilder.FIELD_PROJECT);
158 :
159 : /** Project containing the change, as a prefix field. */
160 155 : public static final IndexedField<ChangeData, String>.SearchSpec PROJECTS_SPEC =
161 155 : PROJECT_FIELD.prefix(ChangeQueryBuilder.FIELD_PROJECTS);
162 :
163 : /** Reference (aka branch) the change will submit onto. */
164 155 : public static final IndexedField<ChangeData, String> REF_FIELD =
165 155 : IndexedField.<ChangeData>stringBuilder("Ref")
166 155 : .required()
167 155 : .size(300)
168 155 : .build(changeGetter(c -> c.getDest().branch()));
169 :
170 155 : public static final IndexedField<ChangeData, String>.SearchSpec REF_SPEC =
171 155 : REF_FIELD.exact(ChangeQueryBuilder.FIELD_REF);
172 :
173 : /** Topic, a short annotation on the branch. */
174 155 : public static final IndexedField<ChangeData, String> TOPIC_FIELD =
175 155 : IndexedField.<ChangeData>stringBuilder("Topic").size(500).build(ChangeField::getTopic);
176 :
177 155 : public static final IndexedField<ChangeData, String>.SearchSpec EXACT_TOPIC =
178 155 : TOPIC_FIELD.exact("topic4");
179 :
180 : /** Topic, a short annotation on the branch. */
181 155 : public static final IndexedField<ChangeData, String>.SearchSpec FUZZY_TOPIC =
182 155 : TOPIC_FIELD.fullText("topic5");
183 :
184 : /** Topic, a short annotation on the branch. */
185 155 : public static final IndexedField<ChangeData, String>.SearchSpec PREFIX_TOPIC =
186 155 : TOPIC_FIELD.prefix("topic6");
187 :
188 : /** {@link com.google.gerrit.entities.SubmissionId} assigned by MergeOp. */
189 155 : public static final IndexedField<ChangeData, String> SUBMISSIONID_FIELD =
190 155 : IndexedField.<ChangeData>stringBuilder("SubmissionId")
191 155 : .size(500)
192 155 : .build(changeGetter(Change::getSubmissionId));
193 :
194 155 : public static final IndexedField<ChangeData, String>.SearchSpec SUBMISSIONID_SPEC =
195 155 : SUBMISSIONID_FIELD.exact(ChangeQueryBuilder.FIELD_SUBMISSIONID);
196 :
197 : /** Last update time since January 1, 1970. */
198 : // TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant
199 155 : public static final FieldDef<ChangeData, Timestamp> UPDATED =
200 155 : timestamp("updated2")
201 155 : .stored()
202 155 : .build(changeGetter(change -> Timestamp.from(change.getLastUpdatedOn())));
203 :
204 : /** When this change was merged, time since January 1, 1970. */
205 : // TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant
206 155 : public static final IndexedField<ChangeData, Timestamp> MERGED_ON_FIELD =
207 155 : IndexedField.<ChangeData>timestampBuilder("MergedOn")
208 155 : .stored()
209 155 : .build(
210 103 : cd -> cd.getMergedOn().map(Timestamp::from).orElse(null),
211 100 : (cd, field) -> cd.setMergedOn(field != null ? field.toInstant() : null));
212 :
213 155 : public static final IndexedField<ChangeData, Timestamp>.SearchSpec MERGED_ON_SPEC =
214 155 : MERGED_ON_FIELD.timestamp(ChangeQueryBuilder.FIELD_MERGED_ON);
215 :
216 : /** List of full file paths modified in the current patch set. */
217 155 : public static final IndexedField<ChangeData, Iterable<String>> PATH_FIELD =
218 : // Named for backwards compatibility.
219 155 : IndexedField.<ChangeData>iterableStringBuilder("File")
220 155 : .build(cd -> firstNonNull(cd.currentFilePaths(), ImmutableList.of()));
221 :
222 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec PATH_SPEC =
223 : PATH_FIELD
224 : // Named for backwards compatibility.
225 155 : .exact(ChangeQueryBuilder.FIELD_FILE);
226 :
227 : public static Set<String> getFileParts(ChangeData cd) {
228 103 : List<String> paths = cd.currentFilePaths();
229 :
230 103 : Splitter s = Splitter.on('/').omitEmptyStrings();
231 103 : Set<String> r = new HashSet<>();
232 103 : for (String path : paths) {
233 90 : for (String part : s.split(path)) {
234 90 : r.add(part);
235 90 : }
236 90 : }
237 103 : return r;
238 : }
239 :
240 : /** Hashtags tied to a change */
241 155 : public static final IndexedField<ChangeData, Iterable<String>> HASHTAG_FIELD =
242 155 : IndexedField.<ChangeData>iterableStringBuilder("Hashtag")
243 155 : .size(200)
244 155 : .build(cd -> cd.hashtags().stream().map(String::toLowerCase).collect(toSet()));
245 :
246 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec HASHTAG_SPEC =
247 155 : HASHTAG_FIELD.exact(ChangeQueryBuilder.FIELD_HASHTAG);
248 :
249 : /** Hashtags as fulltext field for in-string search. */
250 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FUZZY_HASHTAG =
251 155 : HASHTAG_FIELD.fullText("hashtag2");
252 :
253 : /** Hashtags as prefix field for in-string search. */
254 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec PREFIX_HASHTAG =
255 155 : HASHTAG_FIELD.prefix("hashtag3");
256 :
257 : /** Hashtags with original case. */
258 155 : public static final IndexedField<ChangeData, Iterable<byte[]>> HASHTAG_CASE_AWARE_FIELD =
259 155 : IndexedField.<ChangeData>iterableByteArrayBuilder("HashtagCaseAware")
260 155 : .stored()
261 155 : .build(
262 103 : cd -> cd.hashtags().stream().map(t -> t.getBytes(UTF_8)).collect(toSet()),
263 : (cd, field) ->
264 100 : cd.setHashtags(
265 100 : StreamSupport.stream(field.spliterator(), false)
266 100 : .map(f -> new String(f, UTF_8))
267 100 : .collect(toImmutableSet())));
268 :
269 : public static final IndexedField<ChangeData, Iterable<byte[]>>.SearchSpec
270 155 : HASHTAG_CASE_AWARE_SPEC = HASHTAG_CASE_AWARE_FIELD.storedOnly("_hashtag");
271 :
272 : /** Components of each file path modified in the current patch set. */
273 155 : public static final IndexedField<ChangeData, Iterable<String>> FILE_PART_FIELD =
274 155 : IndexedField.<ChangeData>iterableStringBuilder("FilePart").build(ChangeField::getFileParts);
275 :
276 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FILE_PART_SPEC =
277 155 : FILE_PART_FIELD.exact(ChangeQueryBuilder.FIELD_FILEPART);
278 :
279 : /** File extensions of each file modified in the current patch set. */
280 155 : public static final IndexedField<ChangeData, Iterable<String>> EXTENSION_FIELD =
281 155 : IndexedField.<ChangeData>iterableStringBuilder("Extension")
282 155 : .size(100)
283 155 : .build(ChangeField::getExtensions);
284 :
285 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec EXTENSION_SPEC =
286 155 : EXTENSION_FIELD.exact(ChangeQueryBuilder.FIELD_EXTENSION);
287 :
288 : public static Set<String> getExtensions(ChangeData cd) {
289 103 : return extensions(cd).collect(toSet());
290 : }
291 :
292 : /**
293 : * File extensions of each file modified in the current patch set as a sorted list. The purpose of
294 : * this field is to allow matching changes that only touch files with certain file extensions.
295 : */
296 155 : public static final IndexedField<ChangeData, String> ONLY_EXTENSIONS_FIELD =
297 155 : IndexedField.<ChangeData>stringBuilder("OnlyExtensions")
298 155 : .build(ChangeField::getAllExtensionsAsList);
299 :
300 155 : public static final IndexedField<ChangeData, String>.SearchSpec ONLY_EXTENSIONS_SPEC =
301 155 : ONLY_EXTENSIONS_FIELD.exact(ChangeQueryBuilder.FIELD_ONLY_EXTENSIONS);
302 :
303 : public static String getAllExtensionsAsList(ChangeData cd) {
304 103 : return extensions(cd).distinct().sorted().collect(joining(","));
305 : }
306 :
307 : /**
308 : * Returns a stream with all file extensions that are used by files in the given change. A file
309 : * extension is defined as the portion of the filename following the final `.`. Files with no `.`
310 : * in their name have no extension. For them an empty string is returned as part of the stream.
311 : *
312 : * <p>If the change contains multiple files with the same extension the extension is returned
313 : * multiple times in the stream (once per file).
314 : */
315 : private static Stream<String> extensions(ChangeData cd) {
316 103 : return cd.currentFilePaths().stream()
317 : // Use case-insensitive file extensions even though other file fields are case-sensitive.
318 : // If we want to find "all Java files", we want to match both .java and .JAVA, even if we
319 : // normally care about case sensitivity. (Whether we should change the existing file/path
320 : // predicates to be case insensitive is a separate question.)
321 103 : .map(f -> Files.getFileExtension(f).toLowerCase(Locale.US));
322 : }
323 :
324 : /** Footers from the commit message of the current patch set. */
325 155 : public static final IndexedField<ChangeData, Iterable<String>> FOOTER_FIELD =
326 155 : IndexedField.<ChangeData>iterableStringBuilder("Footer").build(ChangeField::getFooters);
327 :
328 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FOOTER_SPEC =
329 155 : FOOTER_FIELD.exact(ChangeQueryBuilder.FIELD_FOOTER);
330 :
331 : public static Set<String> getFooters(ChangeData cd) {
332 103 : return cd.commitFooters().stream()
333 103 : .map(f -> f.toString().toLowerCase(Locale.US))
334 103 : .collect(toSet());
335 : }
336 :
337 : /** Footers from the commit message of the current patch set. */
338 155 : public static final IndexedField<ChangeData, Iterable<String>> FOOTER_NAME_FIELD =
339 155 : IndexedField.<ChangeData>iterableStringBuilder("FooterName")
340 155 : .build(ChangeField::getFootersNames);
341 :
342 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec FOOTER_NAME =
343 155 : FOOTER_NAME_FIELD.exact(ChangeQueryBuilder.FIELD_FOOTER_NAME);
344 :
345 : public static Set<String> getFootersNames(ChangeData cd) {
346 103 : return cd.commitFooters().stream().map(f -> f.getKey()).collect(toSet());
347 : }
348 :
349 : /** Folders that are touched by the current patch set. */
350 155 : public static final IndexedField<ChangeData, Iterable<String>> DIRECTORY_FIELD =
351 155 : IndexedField.<ChangeData>iterableStringBuilder("Directory")
352 155 : .build(ChangeField::getDirectories);
353 :
354 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec DIRECTORY_SPEC =
355 155 : DIRECTORY_FIELD.exact(ChangeQueryBuilder.FIELD_DIRECTORY);
356 :
357 : public static Set<String> getDirectories(ChangeData cd) {
358 103 : List<String> paths = cd.currentFilePaths();
359 :
360 103 : Splitter s = Splitter.on('/').omitEmptyStrings();
361 103 : Set<String> r = new HashSet<>();
362 103 : for (String path : paths) {
363 90 : StringBuilder directory = new StringBuilder();
364 90 : r.add(directory.toString());
365 90 : String nextPart = null;
366 90 : for (String part : s.split(path.toLowerCase(Locale.US))) {
367 90 : if (nextPart != null) {
368 14 : r.add(nextPart);
369 :
370 14 : if (directory.length() > 0) {
371 6 : directory.append("/");
372 : }
373 14 : directory.append(nextPart);
374 :
375 14 : String intermediateDir = directory.toString();
376 14 : int i = intermediateDir.indexOf('/');
377 14 : while (i >= 0) {
378 6 : r.add(intermediateDir);
379 6 : intermediateDir = intermediateDir.substring(i + 1);
380 6 : i = intermediateDir.indexOf('/');
381 : }
382 : }
383 90 : nextPart = part;
384 90 : }
385 90 : }
386 103 : return r;
387 : }
388 :
389 : /** Owner/creator of the change. */
390 155 : public static final IndexedField<ChangeData, Integer> OWNER_FIELD =
391 155 : IndexedField.<ChangeData>integerBuilder("Owner")
392 155 : .required()
393 155 : .build(changeGetter(c -> c.getOwner().get()));
394 :
395 155 : public static final IndexedField<ChangeData, Integer>.SearchSpec OWNER_SPEC =
396 155 : OWNER_FIELD.integer(ChangeQueryBuilder.FIELD_OWNER);
397 :
398 : /** Uploader of the latest patch set. */
399 155 : public static final IndexedField<ChangeData, Integer> UPLOADER_FIELD =
400 155 : IndexedField.<ChangeData>integerBuilder("Uploader")
401 155 : .required()
402 155 : .build(cd -> cd.currentPatchSet().uploader().get());
403 :
404 155 : public static final IndexedField<ChangeData, Integer>.SearchSpec UPLOADER_SPEC =
405 155 : UPLOADER_FIELD.integer(ChangeQueryBuilder.FIELD_UPLOADER);
406 :
407 : /** References the source change number that this change was cherry-picked from. */
408 155 : public static final IndexedField<ChangeData, Integer> CHERRY_PICK_OF_CHANGE_FIELD =
409 155 : IndexedField.<ChangeData>integerBuilder("CherryPickOfChange")
410 155 : .build(
411 : cd ->
412 103 : cd.change().getCherryPickOf() != null
413 10 : ? cd.change().getCherryPickOf().changeId().get()
414 103 : : null);
415 :
416 155 : public static final IndexedField<ChangeData, Integer>.SearchSpec CHERRY_PICK_OF_CHANGE =
417 155 : CHERRY_PICK_OF_CHANGE_FIELD.integer(ChangeQueryBuilder.FIELD_CHERRY_PICK_OF_CHANGE);
418 :
419 : /** References the source change patch-set that this change was cherry-picked from. */
420 155 : public static final IndexedField<ChangeData, Integer> CHERRY_PICK_OF_PATCHSET_FIELD =
421 155 : IndexedField.<ChangeData>integerBuilder("CherryPickOfPatchset")
422 155 : .build(
423 : cd ->
424 103 : cd.change().getCherryPickOf() != null
425 10 : ? cd.change().getCherryPickOf().get()
426 103 : : null);
427 :
428 155 : public static final IndexedField<ChangeData, Integer>.SearchSpec CHERRY_PICK_OF_PATCHSET =
429 155 : CHERRY_PICK_OF_PATCHSET_FIELD.integer(ChangeQueryBuilder.FIELD_CHERRY_PICK_OF_PATCHSET);
430 :
431 : /** This class decouples the internal and API types from storage. */
432 : private static class StoredAttentionSetEntry {
433 : final long timestampMillis;
434 : final int userId;
435 : final String reason;
436 : final AttentionSetUpdate.Operation operation;
437 :
438 51 : StoredAttentionSetEntry(AttentionSetUpdate attentionSetUpdate) {
439 51 : timestampMillis = attentionSetUpdate.timestamp().toEpochMilli();
440 51 : userId = attentionSetUpdate.account().get();
441 51 : reason = attentionSetUpdate.reason();
442 51 : operation = attentionSetUpdate.operation();
443 51 : }
444 :
445 : AttentionSetUpdate toAttentionSetUpdate() {
446 49 : return AttentionSetUpdate.createFromRead(
447 49 : Instant.ofEpochMilli(timestampMillis), Account.id(userId), operation, reason);
448 : }
449 : }
450 :
451 : /**
452 : * Users included in the attention set of the change. This omits timestamp, reason and possible
453 : * future fields.
454 : *
455 : * @see #ATTENTION_SET_FULL
456 : */
457 155 : public static final IndexedField<ChangeData, Iterable<Integer>> ATTENTION_SET_USERS_FIELD =
458 155 : IndexedField.<ChangeData>iterableIntegerBuilder("AttentionSetUsers")
459 155 : .build(ChangeField::getAttentionSetUserIds);
460 :
461 155 : public static final IndexedField<ChangeData, Iterable<Integer>>.SearchSpec ATTENTION_SET_USERS =
462 155 : ATTENTION_SET_USERS_FIELD.integer(ChangeQueryBuilder.FIELD_ATTENTION_SET_USERS);
463 :
464 : /** Number of changes that contain attention set. */
465 155 : public static final IndexedField<ChangeData, Integer> ATTENTION_SET_USERS_COUNT_FIELD =
466 155 : IndexedField.<ChangeData>integerBuilder("AttentionSetUsersCount")
467 155 : .build(cd -> additionsOnly(cd.attentionSet()).size());
468 :
469 155 : public static final IndexedField<ChangeData, Integer>.SearchSpec ATTENTION_SET_USERS_COUNT =
470 155 : ATTENTION_SET_USERS_COUNT_FIELD.integerRange(
471 : ChangeQueryBuilder.FIELD_ATTENTION_SET_USERS_COUNT);
472 :
473 : /**
474 : * The full attention set data including timestamp, reason and possible future fields.
475 : *
476 : * @see #ATTENTION_SET_USERS
477 : */
478 155 : public static final FieldDef<ChangeData, Iterable<byte[]>> ATTENTION_SET_FULL =
479 155 : storedOnly(ChangeQueryBuilder.FIELD_ATTENTION_SET_FULL)
480 155 : .buildRepeatable(
481 : ChangeField::storedAttentionSet,
482 : (cd, value) ->
483 100 : parseAttentionSet(
484 100 : StreamSupport.stream(value.spliterator(), false)
485 100 : .map(v -> new String(v, UTF_8))
486 100 : .collect(toImmutableSet()),
487 : cd));
488 :
489 : /** The user assigned to the change. */
490 155 : public static final IndexedField<ChangeData, Integer> ASSIGNEE_FIELD =
491 155 : IndexedField.<ChangeData>integerBuilder("Assignee")
492 155 : .build(changeGetter(c -> c.getAssignee() != null ? c.getAssignee().get() : NO_ASSIGNEE));
493 :
494 155 : public static final IndexedField<ChangeData, Integer>.SearchSpec ASSIGNEE_SPEC =
495 155 : ASSIGNEE_FIELD.integer(ChangeQueryBuilder.FIELD_ASSIGNEE);
496 :
497 : /** Reviewer(s) associated with the change. */
498 155 : public static final IndexedField<ChangeData, Iterable<String>> REVIEWER_FIELD =
499 155 : IndexedField.<ChangeData>iterableStringBuilder("Reviewer")
500 155 : .stored()
501 155 : .build(
502 103 : cd -> getReviewerFieldValues(cd.reviewers()),
503 100 : (cd, field) -> cd.setReviewers(parseReviewerFieldValues(cd.getId(), field)));
504 :
505 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec REVIEWER_SPEC =
506 155 : REVIEWER_FIELD.exact("reviewer2");
507 :
508 : /** Reviewer(s) associated with the change that do not have a gerrit account. */
509 155 : public static final IndexedField<ChangeData, Iterable<String>> REVIEWER_BY_EMAIL_FIELD =
510 155 : IndexedField.<ChangeData>iterableStringBuilder("ReviewerByEmail")
511 155 : .stored()
512 155 : .build(
513 103 : cd -> getReviewerByEmailFieldValues(cd.reviewersByEmail()),
514 : (cd, field) ->
515 100 : cd.setReviewersByEmail(parseReviewerByEmailFieldValues(cd.getId(), field)));
516 :
517 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec REVIEWER_BY_EMAIL =
518 155 : REVIEWER_BY_EMAIL_FIELD.exact("reviewer_by_email");
519 :
520 : /** Reviewer(s) modified during change's current WIP phase. */
521 155 : public static final IndexedField<ChangeData, Iterable<String>> PENDING_REVIEWER_FIELD =
522 155 : IndexedField.<ChangeData>iterableStringBuilder("PendingReviewer")
523 155 : .stored()
524 155 : .build(
525 103 : cd -> getReviewerFieldValues(cd.pendingReviewers()),
526 100 : (cd, field) -> cd.setPendingReviewers(parseReviewerFieldValues(cd.getId(), field)));
527 :
528 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec PENDING_REVIEWER_SPEC =
529 155 : PENDING_REVIEWER_FIELD.exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER);
530 :
531 : /** Reviewer(s) by email modified during change's current WIP phase. */
532 155 : public static final IndexedField<ChangeData, Iterable<String>> PENDING_REVIEWER_BY_EMAIL_FIELD =
533 155 : IndexedField.<ChangeData>iterableStringBuilder("PendingReviewerByEmail")
534 155 : .stored()
535 155 : .build(
536 103 : cd -> getReviewerByEmailFieldValues(cd.pendingReviewersByEmail()),
537 : (cd, field) ->
538 100 : cd.setPendingReviewersByEmail(
539 100 : parseReviewerByEmailFieldValues(cd.getId(), field)));
540 :
541 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec
542 155 : PENDING_REVIEWER_BY_EMAIL =
543 155 : PENDING_REVIEWER_BY_EMAIL_FIELD.exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER_BY_EMAIL);
544 :
545 : /** References a change that this change reverts. */
546 155 : public static final IndexedField<ChangeData, Integer> REVERT_OF_FIELD =
547 155 : IndexedField.<ChangeData>integerBuilder("RevertOf")
548 155 : .build(cd -> cd.change().getRevertOf() != null ? cd.change().getRevertOf().get() : null);
549 :
550 155 : public static final IndexedField<ChangeData, Integer>.SearchSpec REVERT_OF =
551 155 : REVERT_OF_FIELD.integer(ChangeQueryBuilder.FIELD_REVERTOF);
552 :
553 155 : public static final IndexedField<ChangeData, String> IS_PURE_REVERT_FIELD =
554 155 : IndexedField.<ChangeData>stringBuilder("IsPureRevert")
555 155 : .build(cd -> Boolean.TRUE.equals(cd.isPureRevert()) ? "1" : "0");
556 :
557 155 : public static final IndexedField<ChangeData, String>.SearchSpec IS_PURE_REVERT_SPEC =
558 155 : IS_PURE_REVERT_FIELD.fullText(ChangeQueryBuilder.FIELD_PURE_REVERT);
559 :
560 : /**
561 : * Determines if a change is submittable based on {@link
562 : * com.google.gerrit.entities.SubmitRequirement}s.
563 : */
564 155 : public static final IndexedField<ChangeData, String> IS_SUBMITTABLE_FIELD =
565 155 : IndexedField.<ChangeData>stringBuilder("IsSubmittable")
566 155 : .build(
567 : cd ->
568 : // All submit requirements should be fulfilled
569 103 : cd.submitRequirementsIncludingLegacy().values().stream()
570 103 : .allMatch(SubmitRequirementResult::fulfilled)
571 63 : ? "1"
572 103 : : "0");
573 :
574 155 : public static final IndexedField<ChangeData, String>.SearchSpec IS_SUBMITTABLE_SPEC =
575 155 : IS_SUBMITTABLE_FIELD.exact(ChangeQueryBuilder.FIELD_IS_SUBMITTABLE);
576 :
577 : @VisibleForTesting
578 : static List<String> getReviewerFieldValues(ReviewerSet reviewers) {
579 103 : List<String> r = new ArrayList<>(reviewers.asTable().size() * 2);
580 103 : for (Table.Cell<ReviewerStateInternal, Account.Id, Instant> c : reviewers.asTable().cellSet()) {
581 75 : String v = getReviewerFieldValue(c.getRowKey(), c.getColumnKey());
582 75 : r.add(v);
583 75 : r.add(v + ',' + c.getValue().toEpochMilli());
584 75 : }
585 103 : return r;
586 : }
587 :
588 : public static String getReviewerFieldValue(ReviewerStateInternal state, Account.Id id) {
589 76 : return state.toString() + ',' + id;
590 : }
591 :
592 : @VisibleForTesting
593 : static List<String> getReviewerByEmailFieldValues(ReviewerByEmailSet reviewersByEmail) {
594 103 : List<String> r = new ArrayList<>(reviewersByEmail.asTable().size() * 2);
595 : for (Table.Cell<ReviewerStateInternal, Address, Instant> c :
596 103 : reviewersByEmail.asTable().cellSet()) {
597 10 : String v = getReviewerByEmailFieldValue(c.getRowKey(), c.getColumnKey());
598 10 : r.add(v);
599 10 : if (c.getColumnKey().name() != null) {
600 : // Add another entry without the name to provide search functionality on the email
601 5 : Address emailOnly = Address.create(c.getColumnKey().email());
602 5 : r.add(getReviewerByEmailFieldValue(c.getRowKey(), emailOnly));
603 : }
604 10 : r.add(v + ',' + c.getValue().toEpochMilli());
605 10 : }
606 103 : return r;
607 : }
608 :
609 : public static String getReviewerByEmailFieldValue(ReviewerStateInternal state, Address adr) {
610 11 : return state.toString() + ',' + adr;
611 : }
612 :
613 : public static ReviewerSet parseReviewerFieldValues(Change.Id changeId, Iterable<String> values) {
614 100 : ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Instant> b = ImmutableTable.builder();
615 100 : for (String v : values) {
616 :
617 73 : int i = v.indexOf(',');
618 73 : if (i < 0) {
619 0 : logger.atWarning().log(
620 0 : "Invalid value for reviewer field from change %s: %s", changeId.get(), v);
621 0 : continue;
622 : }
623 :
624 73 : int i2 = v.lastIndexOf(',');
625 73 : if (i2 == i) {
626 : // Don't log a warning here.
627 : // For each reviewer we store 2 values in the reviewer field, one value with the format
628 : // "<reviewer-type>,<account-id>" and one value with the format
629 : // "<reviewer-type>,<account-id>,<timestamp>" (see #getReviewerFieldValues(ReviewerSet)).
630 : // For parsing we are only interested in the "<reviewer-type>,<account-id>,<timestamp>"
631 : // value and the "<reviewer-type>,<account-id>" value is ignored here.
632 73 : continue;
633 : }
634 :
635 73 : Optional<ReviewerStateInternal> reviewerState = getReviewerState(v.substring(0, i));
636 73 : if (!reviewerState.isPresent()) {
637 0 : logger.atWarning().log(
638 : "Failed to parse reviewer state of reviewer field from change %s: %s",
639 0 : changeId.get(), v);
640 0 : continue;
641 : }
642 :
643 73 : Optional<Account.Id> accountId = Account.Id.tryParse(v.substring(i + 1, i2));
644 73 : if (!accountId.isPresent()) {
645 0 : logger.atWarning().log(
646 0 : "Failed to parse account ID of reviewer field from change %s: %s", changeId.get(), v);
647 0 : continue;
648 : }
649 :
650 73 : Long l = Longs.tryParse(v.substring(i2 + 1));
651 73 : if (l == null) {
652 0 : logger.atWarning().log(
653 0 : "Failed to parse timestamp of reviewer field from change %s: %s", changeId.get(), v);
654 0 : continue;
655 : }
656 73 : Instant timestamp = Instant.ofEpochMilli(l);
657 :
658 73 : b.put(reviewerState.get(), accountId.get(), timestamp);
659 73 : }
660 100 : return ReviewerSet.fromTable(b.build());
661 : }
662 :
663 : public static ReviewerByEmailSet parseReviewerByEmailFieldValues(
664 : Change.Id changeId, Iterable<String> values) {
665 100 : ImmutableTable.Builder<ReviewerStateInternal, Address, Instant> b = ImmutableTable.builder();
666 100 : for (String v : values) {
667 10 : int i = v.indexOf(',');
668 10 : if (i < 0) {
669 0 : logger.atWarning().log(
670 0 : "Invalid value for reviewer by email field from change %s: %s", changeId.get(), v);
671 0 : continue;
672 : }
673 :
674 10 : int i2 = v.lastIndexOf(',');
675 10 : if (i2 == i) {
676 : // Don't log a warning here.
677 : // For each reviewer we store 2 values in the reviewer field, one value with the format
678 : // "<reviewer-type>,<email>" and one value with the format
679 : // "<reviewer-type>,<email>,<timestamp>" (see
680 : // #getReviewerByEmailFieldValues(ReviewerByEmailSet)).
681 : // For parsing we are only interested in the "<reviewer-type>,<email>,<timestamp>" value
682 : // and the "<reviewer-type>,<email>" value is ignored here.
683 10 : continue;
684 : }
685 :
686 10 : Optional<ReviewerStateInternal> reviewerState = getReviewerState(v.substring(0, i));
687 10 : if (!reviewerState.isPresent()) {
688 0 : logger.atWarning().log(
689 : "Failed to parse reviewer state of reviewer by email field from change %s: %s",
690 0 : changeId.get(), v);
691 0 : continue;
692 : }
693 :
694 10 : Address address = Address.tryParse(v.substring(i + 1, i2));
695 10 : if (address == null) {
696 0 : logger.atWarning().log(
697 : "Failed to parse address of reviewer by email field from change %s: %s",
698 0 : changeId.get(), v);
699 0 : continue;
700 : }
701 :
702 10 : Long l = Longs.tryParse(v.substring(i2 + 1));
703 10 : if (l == null) {
704 0 : logger.atWarning().log(
705 : "Failed to parse timestamp of reviewer by email field from change %s: %s",
706 0 : changeId.get(), v);
707 0 : continue;
708 : }
709 10 : Instant timestamp = Instant.ofEpochMilli(l);
710 :
711 10 : b.put(reviewerState.get(), address, timestamp);
712 10 : }
713 100 : return ReviewerByEmailSet.fromTable(b.build());
714 : }
715 :
716 : private static Optional<ReviewerStateInternal> getReviewerState(String value) {
717 : try {
718 73 : return Optional.of(ReviewerStateInternal.valueOf(value));
719 0 : } catch (IllegalArgumentException | NullPointerException e) {
720 0 : return Optional.empty();
721 : }
722 : }
723 :
724 : private static ImmutableSet<Integer> getAttentionSetUserIds(ChangeData changeData) {
725 103 : return additionsOnly(changeData.attentionSet()).stream()
726 103 : .map(update -> update.account().get())
727 103 : .collect(toImmutableSet());
728 : }
729 :
730 : private static ImmutableSet<byte[]> storedAttentionSet(ChangeData changeData) {
731 103 : return changeData.attentionSet().stream()
732 103 : .map(StoredAttentionSetEntry::new)
733 103 : .map(storedAttentionSetEntry -> GSON.toJson(storedAttentionSetEntry).getBytes(UTF_8))
734 103 : .collect(toImmutableSet());
735 : }
736 :
737 : /**
738 : * Deserializes the specified attention set entries from JSON and stores them in the specified
739 : * change.
740 : */
741 : public static void parseAttentionSet(
742 : Collection<String> storedAttentionSetEntriesJson, ChangeData changeData) {
743 100 : ImmutableSet<AttentionSetUpdate> attentionSet =
744 100 : storedAttentionSetEntriesJson.stream()
745 100 : .map(
746 49 : entry -> GSON.fromJson(entry, StoredAttentionSetEntry.class).toAttentionSetUpdate())
747 100 : .collect(toImmutableSet());
748 100 : changeData.setAttentionSet(attentionSet);
749 100 : }
750 :
751 : /** Commit ID of any patch set on the change, using prefix match. */
752 155 : public static final FieldDef<ChangeData, Iterable<String>> COMMIT =
753 155 : prefix(ChangeQueryBuilder.FIELD_COMMIT).buildRepeatable(ChangeField::getRevisions);
754 :
755 : /** Commit ID of any patch set on the change, using exact match. */
756 155 : public static final FieldDef<ChangeData, Iterable<String>> EXACT_COMMIT =
757 155 : exact(ChangeQueryBuilder.FIELD_EXACTCOMMIT).buildRepeatable(ChangeField::getRevisions);
758 :
759 : private static ImmutableSet<String> getRevisions(ChangeData cd) {
760 103 : return cd.patchSets().stream().map(ps -> ps.commitId().name()).collect(toImmutableSet());
761 : }
762 :
763 : /** Tracking id extracted from a footer. */
764 155 : public static final FieldDef<ChangeData, Iterable<String>> TR =
765 155 : exact(ChangeQueryBuilder.FIELD_TR)
766 155 : .buildRepeatable(cd -> ImmutableSet.copyOf(cd.trackingFooters().values()));
767 :
768 : /** List of labels on the current patch set including change owner votes. */
769 155 : public static final FieldDef<ChangeData, Iterable<String>> LABEL =
770 155 : exact("label2").buildRepeatable(cd -> getLabels(cd));
771 :
772 : private static Iterable<String> getLabels(ChangeData cd) {
773 103 : Set<String> allApprovals = new HashSet<>();
774 103 : Set<String> distinctApprovals = new HashSet<>();
775 103 : Table<String, Short, Integer> voteCounts = HashBasedTable.create();
776 103 : for (PatchSetApproval a : cd.currentApprovals()) {
777 67 : if (a.value() != 0 && !a.isLegacySubmit()) {
778 58 : increment(voteCounts, a.label(), a.value());
779 58 : Optional<LabelType> labelType = cd.getLabelTypes().byLabel(a.labelId());
780 :
781 58 : allApprovals.add(formatLabel(a.label(), a.value(), a.accountId()));
782 58 : allApprovals.addAll(getMagicLabelFormats(a.label(), a.value(), labelType, a.accountId()));
783 58 : allApprovals.addAll(getLabelOwnerFormats(a, cd, labelType));
784 58 : allApprovals.addAll(getLabelNonUploaderFormats(a, cd, labelType));
785 58 : distinctApprovals.add(formatLabel(a.label(), a.value()));
786 58 : distinctApprovals.addAll(
787 58 : getMagicLabelFormats(a.label(), a.value(), labelType, /* accountId= */ null));
788 : }
789 67 : }
790 103 : allApprovals.addAll(distinctApprovals);
791 103 : allApprovals.addAll(getCountLabelFormats(voteCounts, cd));
792 103 : return allApprovals;
793 : }
794 :
795 : private static void increment(Table<String, Short, Integer> table, String k1, short k2) {
796 58 : if (!table.contains(k1, k2)) {
797 58 : table.put(k1, k2, 1);
798 : } else {
799 18 : int val = table.get(k1, k2);
800 18 : table.put(k1, k2, val + 1);
801 : }
802 58 : }
803 :
804 : private static List<String> getCountLabelFormats(
805 : Table<String, Short, Integer> voteCounts, ChangeData cd) {
806 103 : List<String> allFormats = new ArrayList<>();
807 103 : for (String label : voteCounts.rowMap().keySet()) {
808 58 : Optional<LabelType> labelType = cd.getLabelTypes().byLabel(label);
809 58 : Map<Short, Integer> row = voteCounts.row(label);
810 58 : for (short vote : row.keySet()) {
811 58 : int count = row.get(vote);
812 58 : allFormats.addAll(getCountLabelFormats(labelType, label, vote, count));
813 58 : }
814 58 : }
815 103 : return allFormats;
816 : }
817 :
818 : private static List<String> getCountLabelFormats(
819 : Optional<LabelType> labelType, String label, short vote, int count) {
820 58 : List<String> formats =
821 58 : getMagicLabelFormats(label, vote, labelType, /* accountId= */ null, /* count= */ count);
822 58 : formats.add(formatLabel(label, vote, count));
823 58 : return formats;
824 : }
825 :
826 : /** Get magic label formats corresponding to the {MIN, MAX, ANY} label votes. */
827 : private static List<String> getMagicLabelFormats(
828 : String label, short labelVal, Optional<LabelType> labelType, @Nullable Account.Id accountId) {
829 58 : return getMagicLabelFormats(label, labelVal, labelType, accountId, /* count= */ null);
830 : }
831 :
832 : /** Get magic label formats corresponding to the {MIN, MAX, ANY} label votes. */
833 : private static List<String> getMagicLabelFormats(
834 : String label,
835 : short labelVal,
836 : Optional<LabelType> labelType,
837 : @Nullable Account.Id accountId,
838 : @Nullable Integer count) {
839 58 : List<String> labels = new ArrayList<>();
840 58 : if (labelType.isPresent()) {
841 58 : if (labelVal == labelType.get().getMaxPositive()) {
842 56 : labels.add(formatLabel(label, MagicLabelValue.MAX.name(), accountId, count));
843 : }
844 58 : if (labelVal == labelType.get().getMaxNegative()) {
845 19 : labels.add(formatLabel(label, MagicLabelValue.MIN.name(), accountId, count));
846 : }
847 : }
848 58 : labels.add(formatLabel(label, MagicLabelValue.ANY.name(), accountId, count));
849 58 : return labels;
850 : }
851 :
852 : private static List<String> getLabelOwnerFormats(
853 : PatchSetApproval a, ChangeData cd, Optional<LabelType> labelType) {
854 58 : List<String> allFormats = new ArrayList<>();
855 58 : if (cd.change().getOwner().equals(a.accountId())) {
856 57 : allFormats.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.OWNER_ACCOUNT_ID));
857 57 : allFormats.addAll(
858 57 : getMagicLabelFormats(
859 57 : a.label(), a.value(), labelType, ChangeQueryBuilder.OWNER_ACCOUNT_ID));
860 : }
861 58 : return allFormats;
862 : }
863 :
864 : private static List<String> getLabelNonUploaderFormats(
865 : PatchSetApproval a, ChangeData cd, Optional<LabelType> labelType) {
866 58 : List<String> allFormats = new ArrayList<>();
867 58 : if (!cd.currentPatchSet().uploader().equals(a.accountId())) {
868 28 : allFormats.add(formatLabel(a.label(), a.value(), ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
869 28 : allFormats.addAll(
870 28 : getMagicLabelFormats(
871 28 : a.label(), a.value(), labelType, ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID));
872 : }
873 58 : return allFormats;
874 : }
875 :
876 : public static Set<String> getAuthorParts(ChangeData cd) {
877 103 : return SchemaUtil.getPersonParts(cd.getAuthor());
878 : }
879 :
880 : public static Set<String> getAuthorNameAndEmail(ChangeData cd) {
881 103 : return getNameAndEmail(cd.getAuthor());
882 : }
883 :
884 : public static Set<String> getCommitterParts(ChangeData cd) {
885 103 : return SchemaUtil.getPersonParts(cd.getCommitter());
886 : }
887 :
888 : public static Set<String> getCommitterNameAndEmail(ChangeData cd) {
889 103 : return getNameAndEmail(cd.getCommitter());
890 : }
891 :
892 : private static Set<String> getNameAndEmail(PersonIdent person) {
893 103 : if (person == null) {
894 0 : return ImmutableSet.of();
895 : }
896 :
897 103 : String name = person.getName().toLowerCase(Locale.US);
898 103 : String email = person.getEmailAddress().toLowerCase(Locale.US);
899 :
900 103 : StringBuilder nameEmailBuilder = new StringBuilder();
901 103 : PersonIdent.appendSanitized(nameEmailBuilder, name);
902 103 : nameEmailBuilder.append(" <");
903 103 : PersonIdent.appendSanitized(nameEmailBuilder, email);
904 103 : nameEmailBuilder.append('>');
905 :
906 103 : return ImmutableSet.of(name, email, nameEmailBuilder.toString());
907 : }
908 :
909 : /**
910 : * The exact email address, or any part of the author name or email address, in the current patch
911 : * set.
912 : */
913 155 : public static final IndexedField<ChangeData, Iterable<String>> AUTHOR_PARTS_FIELD =
914 155 : IndexedField.<ChangeData>iterableStringBuilder("AuthorParts")
915 155 : .required()
916 155 : .description(
917 : "The exact email address, or any part of the author name or email address, in the current patch set.")
918 155 : .build(ChangeField::getAuthorParts);
919 :
920 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec AUTHOR_PARTS_SPEC =
921 155 : AUTHOR_PARTS_FIELD.fullText(ChangeQueryBuilder.FIELD_AUTHOR);
922 :
923 : /** The exact name, email address and NameEmail of the author. */
924 155 : public static final IndexedField<ChangeData, Iterable<String>> EXACT_AUTHOR_FIELD =
925 155 : IndexedField.<ChangeData>iterableStringBuilder("ExactAuthor")
926 155 : .required()
927 155 : .description("The exact name, email address and NameEmail of the author.")
928 155 : .build(ChangeField::getAuthorNameAndEmail);
929 :
930 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec EXACT_AUTHOR_SPEC =
931 155 : EXACT_AUTHOR_FIELD.exact(ChangeQueryBuilder.FIELD_EXACTAUTHOR);
932 :
933 : /**
934 : * The exact email address, or any part of the committer name or email address, in the current
935 : * patch set.
936 : */
937 155 : public static final IndexedField<ChangeData, Iterable<String>> COMMITTER_PARTS_FIELD =
938 155 : IndexedField.<ChangeData>iterableStringBuilder("CommitterParts")
939 155 : .description(
940 : "The exact email address, or any part of the committer name or email address, in the current patch set.")
941 155 : .required()
942 155 : .build(ChangeField::getCommitterParts);
943 :
944 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec COMMITTER_PARTS_SPEC =
945 155 : COMMITTER_PARTS_FIELD.fullText(ChangeQueryBuilder.FIELD_COMMITTER);
946 :
947 : /** The exact name, email address, and NameEmail of the committer. */
948 155 : public static final IndexedField<ChangeData, Iterable<String>> EXACT_COMMITTER_FIELD =
949 155 : IndexedField.<ChangeData>iterableStringBuilder("ExactCommiter")
950 155 : .required()
951 155 : .description("The exact name, email address, and NameEmail of the committer.")
952 155 : .build(ChangeField::getCommitterNameAndEmail);
953 :
954 155 : public static final IndexedField<ChangeData, Iterable<String>>.SearchSpec EXACT_COMMITTER_SPEC =
955 155 : EXACT_COMMITTER_FIELD.exact(ChangeQueryBuilder.FIELD_EXACTCOMMITTER);
956 :
957 : /** Serialized change object, used for pre-populating results. */
958 155 : public static final FieldDef<ChangeData, byte[]> CHANGE =
959 155 : storedOnly("_change")
960 155 : .build(
961 155 : changeGetter(change -> toProto(ChangeProtoConverter.INSTANCE, change)),
962 100 : (cd, field) -> cd.setChange(parseProtoFrom(field, ChangeProtoConverter.INSTANCE)));
963 :
964 : /** Serialized approvals for the current patch set, used for pre-populating results. */
965 155 : public static final FieldDef<ChangeData, Iterable<byte[]>> APPROVAL =
966 155 : storedOnly("_approval")
967 155 : .buildRepeatable(
968 103 : cd -> toProtos(PatchSetApprovalProtoConverter.INSTANCE, cd.currentApprovals()),
969 : (cd, field) ->
970 100 : cd.setCurrentApprovals(
971 100 : decodeProtos(field, PatchSetApprovalProtoConverter.INSTANCE)));
972 :
973 : public static String formatLabel(String label, int value) {
974 58 : return formatLabel(label, value, /* accountId= */ null, /* count= */ null);
975 : }
976 :
977 : public static String formatLabel(String label, int value, @Nullable Integer count) {
978 58 : return formatLabel(label, value, /* accountId= */ null, count);
979 : }
980 :
981 : public static String formatLabel(String label, int value, Account.Id accountId) {
982 58 : return formatLabel(label, value, accountId, /* count= */ null);
983 : }
984 :
985 : public static String formatLabel(
986 : String label, int value, @Nullable Account.Id accountId, @Nullable Integer count) {
987 59 : return label.toLowerCase()
988 59 : + (value >= 0 ? "+" : "")
989 : + value
990 59 : + (accountId != null ? "," + formatAccount(accountId) : "")
991 59 : + (count != null ? ",count=" + count : "");
992 : }
993 :
994 : public static String formatLabel(
995 : String label, String value, @Nullable Account.Id accountId, @Nullable Integer count) {
996 59 : return label.toLowerCase()
997 : + "="
998 : + value
999 59 : + (accountId != null ? "," + formatAccount(accountId) : "")
1000 59 : + (count != null ? ",count=" + count : "");
1001 : }
1002 :
1003 : private static String formatAccount(Account.Id accountId) {
1004 58 : if (ChangeQueryBuilder.OWNER_ACCOUNT_ID.equals(accountId)) {
1005 57 : return ChangeQueryBuilder.ARG_ID_OWNER;
1006 58 : } else if (ChangeQueryBuilder.NON_UPLOADER_ACCOUNT_ID.equals(accountId)) {
1007 28 : return ChangeQueryBuilder.ARG_ID_NON_UPLOADER;
1008 : }
1009 58 : return Integer.toString(accountId.get());
1010 : }
1011 :
1012 : /** Commit message of the current patch set. */
1013 155 : public static final FieldDef<ChangeData, String> COMMIT_MESSAGE =
1014 155 : fullText(ChangeQueryBuilder.FIELD_MESSAGE).build(ChangeData::commitMessage);
1015 :
1016 : /** Commit message of the current patch set. */
1017 155 : public static final FieldDef<ChangeData, String> COMMIT_MESSAGE_EXACT =
1018 155 : exact(ChangeQueryBuilder.FIELD_MESSAGE_EXACT)
1019 155 : .build(cd -> truncateStringValueToMaxTermLength(cd.commitMessage()));
1020 :
1021 : /** Summary or inline comment. */
1022 155 : public static final FieldDef<ChangeData, Iterable<String>> COMMENT =
1023 155 : fullText(ChangeQueryBuilder.FIELD_COMMENT)
1024 155 : .buildRepeatable(
1025 : cd ->
1026 103 : Stream.concat(
1027 103 : cd.publishedComments().stream().map(c -> c.message),
1028 : // Some endpoint allow passing user message in input, and we still want to
1029 : // search by that. Index on message template with placeholders for user
1030 : // data, so we don't
1031 : // persist user identifiable information data in index.
1032 103 : cd.messages().stream().map(ChangeMessage::getMessage))
1033 103 : .collect(toSet()));
1034 :
1035 : /** Number of unresolved comment threads of the change, including robot comments. */
1036 155 : public static final FieldDef<ChangeData, Integer> UNRESOLVED_COMMENT_COUNT =
1037 155 : intRange(ChangeQueryBuilder.FIELD_UNRESOLVED_COMMENT_COUNT)
1038 155 : .build(
1039 : ChangeData::unresolvedCommentCount,
1040 100 : (cd, field) -> cd.setUnresolvedCommentCount(field));
1041 :
1042 : /** Total number of published inline comments of the change, including robot comments. */
1043 155 : public static final FieldDef<ChangeData, Integer> TOTAL_COMMENT_COUNT =
1044 155 : intRange("total_comments")
1045 155 : .build(ChangeData::totalCommentCount, (cd, field) -> cd.setTotalCommentCount(field));
1046 :
1047 : /** Whether the change is mergeable. */
1048 155 : public static final FieldDef<ChangeData, String> MERGEABLE =
1049 155 : exact(ChangeQueryBuilder.FIELD_MERGEABLE)
1050 155 : .stored()
1051 155 : .build(
1052 : cd -> {
1053 14 : Boolean m = cd.isMergeable();
1054 14 : if (m == null) {
1055 1 : return null;
1056 : }
1057 14 : return m ? "1" : "0";
1058 : },
1059 100 : (cd, field) -> cd.setMergeable(field == null ? false : field.equals("1")));
1060 :
1061 : /** Whether the change is a merge commit. */
1062 155 : public static final FieldDef<ChangeData, String> MERGE =
1063 155 : exact(ChangeQueryBuilder.FIELD_MERGE)
1064 155 : .stored()
1065 155 : .build(
1066 : cd -> {
1067 103 : Boolean m = cd.isMerge();
1068 103 : if (m == null) {
1069 0 : return null;
1070 : }
1071 103 : return m ? "1" : "0";
1072 : });
1073 :
1074 : /** Whether the change is a cherry pick of another change. */
1075 155 : public static final FieldDef<ChangeData, String> CHERRY_PICK =
1076 155 : exact(ChangeQueryBuilder.FIELD_CHERRYPICK)
1077 155 : .stored()
1078 155 : .build(cd -> cd.change().getCherryPickOf() != null ? "1" : "0");
1079 :
1080 : /** The number of inserted lines in this change. */
1081 155 : public static final FieldDef<ChangeData, Integer> ADDED =
1082 155 : intRange(ChangeQueryBuilder.FIELD_ADDED)
1083 155 : .build(
1084 103 : cd -> cd.changedLines().isPresent() ? cd.changedLines().get().insertions : null,
1085 : (cd, field) -> {
1086 100 : if (field != null) {
1087 100 : cd.setLinesInserted(field);
1088 : }
1089 100 : });
1090 :
1091 : /** The number of deleted lines in this change. */
1092 155 : public static final FieldDef<ChangeData, Integer> DELETED =
1093 155 : intRange(ChangeQueryBuilder.FIELD_DELETED)
1094 155 : .build(
1095 103 : cd -> cd.changedLines().isPresent() ? cd.changedLines().get().deletions : null,
1096 : (cd, field) -> {
1097 100 : if (field != null) {
1098 100 : cd.setLinesDeleted(field);
1099 : }
1100 100 : });
1101 :
1102 : /** The total number of modified lines in this change. */
1103 155 : public static final FieldDef<ChangeData, Integer> DELTA =
1104 155 : intRange(ChangeQueryBuilder.FIELD_DELTA)
1105 155 : .build(cd -> cd.changedLines().map(c -> c.insertions + c.deletions).orElse(null));
1106 :
1107 : /** Determines if this change is private. */
1108 155 : public static final FieldDef<ChangeData, String> PRIVATE =
1109 155 : exact(ChangeQueryBuilder.FIELD_PRIVATE).build(cd -> cd.change().isPrivate() ? "1" : "0");
1110 :
1111 : /** Determines if this change is work in progress. */
1112 155 : public static final FieldDef<ChangeData, String> WIP =
1113 155 : exact(ChangeQueryBuilder.FIELD_WIP).build(cd -> cd.change().isWorkInProgress() ? "1" : "0");
1114 :
1115 : /** Determines if this change has started review. */
1116 155 : public static final FieldDef<ChangeData, String> STARTED =
1117 155 : exact(ChangeQueryBuilder.FIELD_STARTED)
1118 155 : .build(cd -> cd.change().hasReviewStarted() ? "1" : "0");
1119 :
1120 : /** Users who have commented on this change. */
1121 155 : public static final FieldDef<ChangeData, Iterable<Integer>> COMMENTBY =
1122 155 : integer(ChangeQueryBuilder.FIELD_COMMENTBY)
1123 155 : .buildRepeatable(
1124 : cd ->
1125 103 : Stream.concat(
1126 103 : cd.messages().stream().map(ChangeMessage::getAuthor),
1127 103 : cd.publishedComments().stream().map(c -> c.author.getId()))
1128 103 : .filter(Objects::nonNull)
1129 103 : .map(Account.Id::get)
1130 103 : .collect(toSet()));
1131 :
1132 : /** Star labels on this change in the format: <account-id>:<label> */
1133 155 : public static final FieldDef<ChangeData, Iterable<String>> STAR =
1134 155 : exact(ChangeQueryBuilder.FIELD_STAR)
1135 155 : .stored()
1136 155 : .buildRepeatable(
1137 : cd ->
1138 3 : Iterables.transform(
1139 3 : cd.stars().entries(),
1140 : e ->
1141 0 : StarredChangesUtil.StarField.create(e.getKey(), e.getValue()).toString()),
1142 : (cd, field) ->
1143 3 : cd.setStars(
1144 3 : StreamSupport.stream(field.spliterator(), false)
1145 3 : .map(f -> StarredChangesUtil.StarField.parse(f))
1146 3 : .collect(toImmutableListMultimap(e -> e.accountId(), e -> e.label()))));
1147 :
1148 : /** Users that have starred the change with any label. */
1149 155 : public static final FieldDef<ChangeData, Iterable<Integer>> STARBY =
1150 155 : integer(ChangeQueryBuilder.FIELD_STARBY)
1151 155 : .buildRepeatable(cd -> Iterables.transform(cd.stars().keySet(), Account.Id::get));
1152 :
1153 : /** Opaque group identifiers for this change's patch sets. */
1154 155 : public static final FieldDef<ChangeData, Iterable<String>> GROUP =
1155 155 : exact(ChangeQueryBuilder.FIELD_GROUP)
1156 155 : .buildRepeatable(
1157 103 : cd -> cd.patchSets().stream().flatMap(ps -> ps.groups().stream()).collect(toSet()));
1158 :
1159 : /** Serialized patch set object, used for pre-populating results. */
1160 155 : public static final FieldDef<ChangeData, Iterable<byte[]>> PATCH_SET =
1161 155 : storedOnly("_patch_set")
1162 155 : .buildRepeatable(
1163 103 : cd -> toProtos(PatchSetProtoConverter.INSTANCE, cd.patchSets()),
1164 100 : (cd, field) -> cd.setPatchSets(decodeProtos(field, PatchSetProtoConverter.INSTANCE)));
1165 :
1166 : /** Users who have edits on this change. */
1167 155 : public static final FieldDef<ChangeData, Iterable<Integer>> EDITBY =
1168 155 : integer(ChangeQueryBuilder.FIELD_EDITBY)
1169 155 : .buildRepeatable(cd -> cd.editsByUser().stream().map(Account.Id::get).collect(toSet()));
1170 :
1171 : /** Users who have draft comments on this change. */
1172 155 : public static final FieldDef<ChangeData, Iterable<Integer>> DRAFTBY =
1173 155 : integer(ChangeQueryBuilder.FIELD_DRAFTBY)
1174 155 : .buildRepeatable(cd -> cd.draftsByUser().stream().map(Account.Id::get).collect(toSet()));
1175 :
1176 155 : public static final Integer NOT_REVIEWED = -1;
1177 :
1178 : /**
1179 : * Users the change was reviewed by since the last author update.
1180 : *
1181 : * <p>A change is considered reviewed by a user if the latest update by that user is newer than
1182 : * the latest update by the change author. Both top-level change messages and new patch sets are
1183 : * considered to be updates.
1184 : *
1185 : * <p>If the latest update is by the change owner, then the special value {@link #NOT_REVIEWED} is
1186 : * emitted.
1187 : */
1188 155 : public static final FieldDef<ChangeData, Iterable<Integer>> REVIEWEDBY =
1189 155 : integer(ChangeQueryBuilder.FIELD_REVIEWEDBY)
1190 155 : .stored()
1191 155 : .buildRepeatable(
1192 : cd -> {
1193 103 : Set<Account.Id> reviewedBy = cd.reviewedBy();
1194 103 : if (reviewedBy.isEmpty()) {
1195 103 : return ImmutableSet.of(NOT_REVIEWED);
1196 : }
1197 41 : return reviewedBy.stream().map(Account.Id::get).collect(toList());
1198 : },
1199 : (cd, field) ->
1200 100 : cd.setReviewedBy(
1201 100 : StreamSupport.stream(field.spliterator(), false)
1202 100 : .map(Account::id)
1203 100 : .collect(toImmutableSet())));
1204 :
1205 : public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT =
1206 155 : SubmitRuleOptions.builder().recomputeOnClosedChanges(true).build();
1207 :
1208 : public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_STRICT =
1209 155 : SubmitRuleOptions.builder().build();
1210 :
1211 : /** All submit rules results in the form of "$ruleName,$status". */
1212 155 : public static final FieldDef<ChangeData, Iterable<String>> SUBMIT_RULE_RESULT =
1213 155 : exact("submit_rule_result")
1214 155 : .buildRepeatable(
1215 : cd -> {
1216 103 : List<String> result = new ArrayList<>();
1217 103 : List<SubmitRecord> submitRecords = cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT);
1218 103 : for (SubmitRecord record : submitRecords) {
1219 103 : result.add(record.ruleName + "=" + record.status.name());
1220 103 : }
1221 103 : return result;
1222 : });
1223 :
1224 : /**
1225 : * JSON type for storing SubmitRecords.
1226 : *
1227 : * <p>Stored fields need to use a stable format over a long period; this type insulates the index
1228 : * from implementation changes in SubmitRecord itself.
1229 : */
1230 : public static class StoredSubmitRecord {
1231 101 : static class StoredLabel {
1232 : String label;
1233 : SubmitRecord.Label.Status status;
1234 : Integer appliedBy;
1235 : }
1236 :
1237 4 : static class StoredRequirement {
1238 : String fallbackText;
1239 : String type;
1240 : @Deprecated Map<String, String> data;
1241 : }
1242 :
1243 : String ruleName;
1244 : SubmitRecord.Status status;
1245 : List<StoredLabel> labels;
1246 : List<StoredRequirement> requirements;
1247 : String errorMessage;
1248 :
1249 101 : public StoredSubmitRecord(SubmitRecord rec) {
1250 101 : this.ruleName = rec.ruleName;
1251 101 : this.status = rec.status;
1252 101 : this.errorMessage = rec.errorMessage;
1253 101 : if (rec.labels != null) {
1254 101 : this.labels = new ArrayList<>(rec.labels.size());
1255 101 : for (SubmitRecord.Label label : rec.labels) {
1256 101 : StoredLabel sl = new StoredLabel();
1257 101 : sl.label = label.label;
1258 101 : sl.status = label.status;
1259 101 : sl.appliedBy = label.appliedBy != null ? label.appliedBy.get() : null;
1260 101 : this.labels.add(sl);
1261 101 : }
1262 : }
1263 101 : if (rec.requirements != null) {
1264 4 : this.requirements = new ArrayList<>(rec.requirements.size());
1265 4 : for (LegacySubmitRequirement requirement : rec.requirements) {
1266 4 : StoredRequirement sr = new StoredRequirement();
1267 4 : sr.type = requirement.type();
1268 4 : sr.fallbackText = requirement.fallbackText();
1269 : // For backwards compatibility, write an empty map to the index.
1270 : // This is required, because the LegacySubmitRequirement AutoValue can't
1271 : // handle null in the old code.
1272 : // TODO(hiesel): Remove once we have rolled out the new code
1273 : // and waited long enough to not need to roll back.
1274 4 : sr.data = ImmutableMap.of();
1275 4 : this.requirements.add(sr);
1276 4 : }
1277 : }
1278 101 : }
1279 :
1280 : public SubmitRecord toSubmitRecord() {
1281 100 : SubmitRecord rec = new SubmitRecord();
1282 100 : rec.ruleName = ruleName;
1283 100 : rec.status = status;
1284 100 : rec.errorMessage = errorMessage;
1285 100 : if (labels != null) {
1286 100 : rec.labels = new ArrayList<>(labels.size());
1287 100 : for (StoredLabel label : labels) {
1288 100 : SubmitRecord.Label srl = new SubmitRecord.Label();
1289 100 : srl.label = label.label;
1290 100 : srl.status = label.status;
1291 100 : srl.appliedBy = label.appliedBy != null ? Account.id(label.appliedBy) : null;
1292 100 : rec.labels.add(srl);
1293 100 : }
1294 : }
1295 100 : if (requirements != null) {
1296 4 : rec.requirements = new ArrayList<>(requirements.size());
1297 4 : for (StoredRequirement req : requirements) {
1298 : LegacySubmitRequirement sr =
1299 4 : LegacySubmitRequirement.builder()
1300 4 : .setType(req.type)
1301 4 : .setFallbackText(req.fallbackText)
1302 4 : .build();
1303 4 : rec.requirements.add(sr);
1304 4 : }
1305 : }
1306 100 : return rec;
1307 : }
1308 : }
1309 :
1310 155 : public static final FieldDef<ChangeData, Iterable<String>> SUBMIT_RECORD =
1311 155 : exact("submit_record").buildRepeatable(ChangeField::formatSubmitRecordValues);
1312 :
1313 155 : public static final FieldDef<ChangeData, Iterable<byte[]>> STORED_SUBMIT_RECORD_STRICT =
1314 155 : storedOnly("full_submit_record_strict")
1315 155 : .buildRepeatable(
1316 103 : cd -> storedSubmitRecords(cd, SUBMIT_RULE_OPTIONS_STRICT),
1317 : (cd, field) ->
1318 100 : parseSubmitRecords(
1319 100 : StreamSupport.stream(field.spliterator(), false)
1320 100 : .map(f -> new String(f, UTF_8))
1321 100 : .collect(toSet()),
1322 : SUBMIT_RULE_OPTIONS_STRICT,
1323 : cd));
1324 :
1325 155 : public static final FieldDef<ChangeData, Iterable<byte[]>> STORED_SUBMIT_RECORD_LENIENT =
1326 155 : storedOnly("full_submit_record_lenient")
1327 155 : .buildRepeatable(
1328 103 : cd -> storedSubmitRecords(cd, SUBMIT_RULE_OPTIONS_LENIENT),
1329 : (cd, field) ->
1330 100 : parseSubmitRecords(
1331 100 : StreamSupport.stream(field.spliterator(), false)
1332 100 : .map(f -> new String(f, UTF_8))
1333 100 : .collect(toSet()),
1334 : SUBMIT_RULE_OPTIONS_LENIENT,
1335 : cd));
1336 :
1337 : public static void parseSubmitRecords(
1338 : Collection<String> values, SubmitRuleOptions opts, ChangeData out) {
1339 100 : List<SubmitRecord> records = parseSubmitRecords(values);
1340 100 : out.setSubmitRecords(opts, records);
1341 100 : }
1342 :
1343 : @VisibleForTesting
1344 : static List<SubmitRecord> parseSubmitRecords(Collection<String> values) {
1345 100 : return values.stream()
1346 100 : .map(v -> GSON.fromJson(v, StoredSubmitRecord.class).toSubmitRecord())
1347 100 : .collect(toList());
1348 : }
1349 :
1350 : @VisibleForTesting
1351 : static List<byte[]> storedSubmitRecords(List<SubmitRecord> records) {
1352 103 : return Lists.transform(records, r -> GSON.toJson(new StoredSubmitRecord(r)).getBytes(UTF_8));
1353 : }
1354 :
1355 : private static Iterable<byte[]> storedSubmitRecords(ChangeData cd, SubmitRuleOptions opts) {
1356 103 : return storedSubmitRecords(cd.submitRecords(opts));
1357 : }
1358 :
1359 : public static List<String> formatSubmitRecordValues(ChangeData cd) {
1360 103 : Set<String> submitRecordValues = new HashSet<>();
1361 103 : submitRecordValues.addAll(
1362 103 : formatSubmitRecordValues(
1363 103 : cd.submitRecords(SUBMIT_RULE_OPTIONS_STRICT), cd.change().getOwner()));
1364 : // Also backfill results of submit requirements such that users can query submit requirement
1365 : // results using the label operator, for example a query with "label:CR=NEED" will match with
1366 : // changes that have a submit-requirement with name="CR" and status=UNSATISFIED.
1367 : // Reason: We are preserving backward compatibility of the operators `label:$name=$status`
1368 : // which were previously working with submit records. Now admins can configure submit
1369 : // requirements and continue querying them with the label operator.
1370 103 : submitRecordValues.addAll(formatSubmitRequirementValues(cd.submitRequirements().values()));
1371 103 : return submitRecordValues.stream().collect(Collectors.toList());
1372 : }
1373 :
1374 : @VisibleForTesting
1375 : static List<String> formatSubmitRecordValues(List<SubmitRecord> records, Account.Id changeOwner) {
1376 103 : List<String> result = new ArrayList<>();
1377 103 : for (SubmitRecord rec : records) {
1378 103 : result.add(rec.status.name());
1379 103 : if (rec.labels == null) {
1380 13 : continue;
1381 : }
1382 103 : for (SubmitRecord.Label label : rec.labels) {
1383 103 : String sl = label.status.toString() + ',' + label.label.toLowerCase();
1384 103 : result.add(sl);
1385 103 : String slc = sl + ',';
1386 103 : if (label.appliedBy != null) {
1387 58 : result.add(slc + label.appliedBy.get());
1388 58 : if (label.appliedBy.equals(changeOwner)) {
1389 57 : result.add(slc + ChangeQueryBuilder.OWNER_ACCOUNT_ID.get());
1390 : }
1391 : }
1392 103 : }
1393 103 : }
1394 103 : return result;
1395 : }
1396 :
1397 : /**
1398 : * Generate submit requirement result formats that are compatible with the legacy submit record
1399 : * statuses.
1400 : */
1401 : @VisibleForTesting
1402 : static List<String> formatSubmitRequirementValues(Collection<SubmitRequirementResult> srResults) {
1403 103 : List<String> result = new ArrayList<>();
1404 103 : for (SubmitRequirementResult srResult : srResults) {
1405 3 : switch (srResult.status()) {
1406 : case SATISFIED:
1407 : case OVERRIDDEN:
1408 : case FORCED:
1409 3 : result.add(
1410 3 : SubmitRecord.Label.Status.OK.name()
1411 : + ","
1412 3 : + srResult.submitRequirement().name().toLowerCase());
1413 3 : result.add(
1414 3 : SubmitRecord.Label.Status.MAY.name()
1415 : + ","
1416 3 : + srResult.submitRequirement().name().toLowerCase());
1417 3 : break;
1418 : case UNSATISFIED:
1419 3 : result.add(
1420 3 : SubmitRecord.Label.Status.NEED.name()
1421 : + ","
1422 3 : + srResult.submitRequirement().name().toLowerCase());
1423 3 : result.add(
1424 3 : SubmitRecord.Label.Status.REJECT.name()
1425 : + ","
1426 3 : + srResult.submitRequirement().name().toLowerCase());
1427 3 : break;
1428 : case NOT_APPLICABLE:
1429 : case ERROR:
1430 1 : result.add(
1431 1 : SubmitRecord.Label.Status.IMPOSSIBLE.name()
1432 : + ","
1433 1 : + srResult.submitRequirement().name().toLowerCase());
1434 : }
1435 3 : }
1436 103 : return result;
1437 : }
1438 :
1439 : /** Serialized submit requirements, used for pre-populating results. */
1440 155 : public static final FieldDef<ChangeData, Iterable<byte[]>> STORED_SUBMIT_REQUIREMENTS =
1441 155 : storedOnly("full_submit_requirements")
1442 155 : .buildRepeatable(
1443 : cd ->
1444 103 : toProtos(
1445 103 : SubmitRequirementProtoConverter.INSTANCE, cd.submitRequirements().values()),
1446 100 : (cd, field) -> parseSubmitRequirements(field, cd));
1447 :
1448 : private static void parseSubmitRequirements(Iterable<byte[]> values, ChangeData out) {
1449 100 : out.setSubmitRequirements(
1450 100 : StreamSupport.stream(values.spliterator(), false)
1451 100 : .map(
1452 : f ->
1453 2 : SubmitRequirementProtoConverter.INSTANCE.fromProto(
1454 2 : Protos.parseUnchecked(
1455 2 : SubmitRequirementProtoConverter.INSTANCE.getParser(), f)))
1456 100 : .filter(sr -> !sr.isLegacy())
1457 100 : .collect(
1458 100 : ImmutableMap.toImmutableMap(sr -> sr.submitRequirement(), Function.identity())));
1459 100 : }
1460 :
1461 : /**
1462 : * All values of all refs that were used in the course of indexing this document.
1463 : *
1464 : * <p>Emitted as UTF-8 encoded strings of the form {@code project:ref/name:[hex sha]}.
1465 : */
1466 155 : public static final FieldDef<ChangeData, Iterable<byte[]>> REF_STATE =
1467 155 : storedOnly("ref_state")
1468 155 : .buildRepeatable(
1469 : cd -> {
1470 103 : List<byte[]> result = new ArrayList<>();
1471 103 : cd.getRefStates()
1472 103 : .entries()
1473 103 : .forEach(e -> result.add(e.getValue().toByteArray(e.getKey())));
1474 103 : return result;
1475 : },
1476 100 : (cd, field) -> cd.setRefStates(RefState.parseStates(field)));
1477 :
1478 : /**
1479 : * All ref wildcard patterns that were used in the course of indexing this document.
1480 : *
1481 : * <p>Emitted as UTF-8 encoded strings of the form {@code project:ref/name/*}. See {@link
1482 : * RefStatePattern} for the pattern format.
1483 : */
1484 155 : public static final FieldDef<ChangeData, Iterable<byte[]>> REF_STATE_PATTERN =
1485 155 : storedOnly("ref_state_pattern")
1486 155 : .buildRepeatable(
1487 : cd -> {
1488 103 : Change.Id id = cd.getId();
1489 103 : Project.NameKey project = cd.change().getProject();
1490 103 : List<byte[]> result = new ArrayList<>(3);
1491 103 : result.add(
1492 103 : RefStatePattern.create(
1493 : RefNames.REFS_USERS + "*/" + RefNames.EDIT_PREFIX + id + "/*")
1494 103 : .toByteArray(project));
1495 103 : result.add(
1496 103 : RefStatePattern.create(RefNames.refsStarredChangesPrefix(id) + "*")
1497 103 : .toByteArray(allUsers(cd)));
1498 103 : result.add(
1499 103 : RefStatePattern.create(RefNames.refsDraftCommentsPrefix(id) + "*")
1500 103 : .toByteArray(allUsers(cd)));
1501 103 : return result;
1502 : },
1503 100 : (cd, field) -> cd.setRefStatePatterns(field));
1504 :
1505 : @Nullable
1506 : private static String getTopic(ChangeData cd) {
1507 103 : Change c = cd.change();
1508 103 : if (c == null) {
1509 0 : return null;
1510 : }
1511 103 : return firstNonNull(c.getTopic(), "");
1512 : }
1513 :
1514 : private static <T> List<byte[]> toProtos(ProtoConverter<?, T> converter, Collection<T> objects) {
1515 103 : return objects.stream().map(object -> toProto(converter, object)).collect(toImmutableList());
1516 : }
1517 :
1518 : private static <T> byte[] toProto(ProtoConverter<?, T> converter, T object) {
1519 103 : return Protos.toByteArray(converter.toProto(object));
1520 : }
1521 :
1522 : private static <T> List<T> decodeProtos(Iterable<byte[]> raw, ProtoConverter<?, T> converter) {
1523 100 : return StreamSupport.stream(raw.spliterator(), false)
1524 100 : .map(bytes -> parseProtoFrom(bytes, converter))
1525 100 : .collect(toImmutableList());
1526 : }
1527 :
1528 : private static <P extends MessageLite, T> T parseProtoFrom(
1529 : byte[] bytes, ProtoConverter<P, T> converter) {
1530 100 : P message = Protos.parseUnchecked(converter.getParser(), bytes, 0, bytes.length);
1531 100 : return converter.fromProto(message);
1532 : }
1533 :
1534 : private static <T> SchemaFieldDefs.Getter<ChangeData, T> changeGetter(Function<Change, T> func) {
1535 155 : return in -> in.change() != null ? func.apply(in.change()) : null;
1536 : }
1537 :
1538 : private static AllUsersName allUsers(ChangeData cd) {
1539 103 : return cd.getAllUsersNameForIndexing();
1540 : }
1541 :
1542 : private static String truncateStringValueToMaxTermLength(String str) {
1543 103 : return truncateStringValue(str, MAX_TERM_LENGTH);
1544 : }
1545 :
1546 : @VisibleForTesting
1547 : static String truncateStringValue(String str, int maxBytes) {
1548 103 : if (maxBytes < 0) {
1549 0 : throw new IllegalArgumentException("maxBytes < 0 not allowed");
1550 : }
1551 :
1552 103 : if (maxBytes == 0) {
1553 1 : return "";
1554 : }
1555 :
1556 103 : if (str.length() > maxBytes) {
1557 1 : if (Character.isHighSurrogate(str.charAt(maxBytes - 1))) {
1558 1 : str = str.substring(0, maxBytes - 1);
1559 : } else {
1560 1 : str = str.substring(0, maxBytes);
1561 : }
1562 : }
1563 103 : byte[] strBytes = str.getBytes(UTF_8);
1564 103 : if (strBytes.length > maxBytes) {
1565 1 : while (maxBytes > 0 && (strBytes[maxBytes] & 0xC0) == 0x80) {
1566 1 : maxBytes -= 1;
1567 : }
1568 1 : if (maxBytes > 0) {
1569 1 : if (strBytes.length >= maxBytes && (strBytes[maxBytes - 1] & 0xE0) == 0xC0) {
1570 0 : maxBytes -= 1;
1571 : }
1572 1 : if (strBytes.length >= maxBytes && (strBytes[maxBytes - 1] & 0xF0) == 0xE0) {
1573 0 : maxBytes -= 1;
1574 : }
1575 1 : if (strBytes.length >= maxBytes && (strBytes[maxBytes - 1] & 0xF8) == 0xF0) {
1576 0 : maxBytes -= 1;
1577 : }
1578 : }
1579 1 : return new String(Arrays.copyOfRange(strBytes, 0, maxBytes), UTF_8);
1580 : }
1581 103 : return str;
1582 : }
1583 : }
|