Merge "Bind DiffOperations for batch programs"
diff --git a/Documentation/js_licenses.txt b/Documentation/js_licenses.txt
index e14df57..9ac438a 100644
--- a/Documentation/js_licenses.txt
+++ b/Documentation/js_licenses.txt
@@ -1464,219 +1464,6 @@
----
-[[shadow-selection-polyfill]]
-shadow-selection-polyfill
-
-* shadow-selection-polyfill
-
-[[shadow-selection-polyfill_license]]
-----
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- 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.
-
-----
-
-
[[tslib]]
tslib
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 1d96189..1bff25c 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -4423,219 +4423,6 @@
----
-[[shadow-selection-polyfill]]
-shadow-selection-polyfill
-
-* shadow-selection-polyfill
-
-[[shadow-selection-polyfill_license]]
-----
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- 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.
-
-----
-
-
[[tslib]]
tslib
diff --git a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
index 44a377a..562464d 100644
--- a/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
+++ b/java/com/google/gerrit/elasticsearch/AbstractElasticIndex.java
@@ -90,7 +90,7 @@
protected static final String SEARCH = "_search";
protected static final String SETTINGS = "settings";
- protected static byte[] decodeBase64(String base64String) {
+ static byte[] decodeBase64(String base64String) {
return BaseEncoding.base64().decode(base64String);
}
diff --git a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
index 162654d..7d4e0c7 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticChangeIndex.java
@@ -14,55 +14,35 @@
package com.google.gerrit.elasticsearch;
-import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.MultimapBuilder;
-import com.google.common.collect.Sets;
import com.google.gerrit.elasticsearch.ElasticMapping.MappingProperties;
import com.google.gerrit.elasticsearch.bulk.BulkRequest;
import com.google.gerrit.elasticsearch.bulk.IndexRequest;
import com.google.gerrit.elasticsearch.bulk.UpdateRequest;
-import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.converter.ChangeProtoConverter;
-import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
-import com.google.gerrit.entities.converter.PatchSetProtoConverter;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.QueryOptions;
-import com.google.gerrit.index.RefState;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.query.DataSource;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
-import com.google.gerrit.server.ReviewerByEmailSet;
-import com.google.gerrit.server.ReviewerSet;
-import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.change.MergeabilityComputationBehavior;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.index.IndexUtils;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.index.change.ChangeIndex;
-import com.google.gerrit.server.project.SubmitRuleOptions;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import java.sql.Timestamp;
-import java.time.Instant;
-import java.time.format.DateTimeFormatter;
-import java.util.Collections;
-import java.util.Optional;
import java.util.Set;
import org.apache.http.HttpStatus;
import org.eclipse.jgit.lib.Config;
@@ -190,242 +170,12 @@
changeDataFactory.create(
parseProtoFrom(decodeBase64(c.getAsString()), ChangeProtoConverter.INSTANCE));
- // Any decoding that is done here must also be done in {@link LuceneChangeIndex}.
-
- // Patch sets.
- cd.setPatchSets(
- decodeProtos(source, ChangeField.PATCH_SET.getName(), PatchSetProtoConverter.INSTANCE));
-
- // Approvals.
- if (source.get(ChangeField.APPROVAL.getName()) != null) {
- cd.setCurrentApprovals(
- decodeProtos(
- source, ChangeField.APPROVAL.getName(), PatchSetApprovalProtoConverter.INSTANCE));
- } else if (fields.contains(ChangeField.APPROVAL.getName())) {
- cd.setCurrentApprovals(Collections.emptyList());
- }
-
- // Added & Deleted.
- JsonElement addedElement = source.get(ChangeField.ADDED.getName());
- JsonElement deletedElement = source.get(ChangeField.DELETED.getName());
- if (addedElement != null && deletedElement != null) {
- // Changed lines.
- int added = addedElement.getAsInt();
- int deleted = deletedElement.getAsInt();
- cd.setChangedLines(added, deleted);
- }
-
- // Star.
- JsonElement starredElement = source.get(ChangeField.STAR.getName());
- if (starredElement != null) {
- ListMultimap<Account.Id, String> stars = MultimapBuilder.hashKeys().arrayListValues().build();
- JsonArray starBy = starredElement.getAsJsonArray();
- if (starBy.size() > 0) {
- for (int i = 0; i < starBy.size(); i++) {
- String[] indexableFields = starBy.get(i).getAsString().split(":");
- Optional<Account.Id> id = Account.Id.tryParse(indexableFields[0]);
- if (id.isPresent()) {
- stars.put(id.get(), indexableFields[1]);
- }
- }
+ for (FieldDef<ChangeData, ?> field : getSchema().getFields().values()) {
+ if (fields.contains(field.getName()) && source.get(field.getName()) != null) {
+ field.setIfPossible(cd, new ElasticStoredValue(source.get(field.getName())));
}
- cd.setStars(stars);
- }
-
- // Mergeable.
- JsonElement mergeableElement = source.get(ChangeField.MERGEABLE.getName());
- if (mergeableElement != null && !skipFields.contains(ChangeField.MERGEABLE.getName())) {
- String mergeable = mergeableElement.getAsString();
- if ("1".equals(mergeable)) {
- cd.setMergeable(true);
- } else if ("0".equals(mergeable)) {
- cd.setMergeable(false);
- }
- }
-
- // Reviewed-by.
- if (source.get(ChangeField.REVIEWEDBY.getName()) != null) {
- JsonArray reviewedBy = source.get(ChangeField.REVIEWEDBY.getName()).getAsJsonArray();
- if (reviewedBy.size() > 0) {
- Set<Account.Id> accounts = Sets.newHashSetWithExpectedSize(reviewedBy.size());
- for (int i = 0; i < reviewedBy.size(); i++) {
- int aId = reviewedBy.get(i).getAsInt();
- if (reviewedBy.size() == 1 && aId == ChangeField.NOT_REVIEWED) {
- break;
- }
- accounts.add(Account.id(aId));
- }
- cd.setReviewedBy(accounts);
- }
- } else if (fields.contains(ChangeField.REVIEWEDBY.getName())) {
- cd.setReviewedBy(Collections.emptySet());
- }
-
- // Hashtag.
- if (source.get(ChangeField.HASHTAG.getName()) != null) {
- JsonArray hashtagArray = source.get(ChangeField.HASHTAG.getName()).getAsJsonArray();
- if (hashtagArray.size() > 0) {
- Set<String> hashtags = Sets.newHashSetWithExpectedSize(hashtagArray.size());
- for (int i = 0; i < hashtagArray.size(); i++) {
- hashtags.add(hashtagArray.get(i).getAsString());
- }
- cd.setHashtags(hashtags);
- }
- } else if (fields.contains(ChangeField.HASHTAG.getName())) {
- cd.setHashtags(Collections.emptySet());
- }
-
- // Star.
- if (source.get(ChangeField.STAR.getName()) != null) {
- JsonArray starArray = source.get(ChangeField.STAR.getName()).getAsJsonArray();
- if (starArray.size() > 0) {
- ListMultimap<Account.Id, String> stars =
- MultimapBuilder.hashKeys().arrayListValues().build();
- for (int i = 0; i < starArray.size(); i++) {
- StarredChangesUtil.StarField starField =
- StarredChangesUtil.StarField.parse(starArray.get(i).getAsString());
- stars.put(starField.accountId(), starField.label());
- }
- cd.setStars(stars);
- }
- } else if (fields.contains(ChangeField.STAR.getName())) {
- cd.setStars(ImmutableListMultimap.of());
- }
-
- // Reviewer.
- if (source.get(ChangeField.REVIEWER.getName()) != null) {
- cd.setReviewers(
- ChangeField.parseReviewerFieldValues(
- cd.getId(),
- FluentIterable.from(source.get(ChangeField.REVIEWER.getName()).getAsJsonArray())
- .transform(JsonElement::getAsString)));
- } else if (fields.contains(ChangeField.REVIEWER.getName())) {
- cd.setReviewers(ReviewerSet.empty());
- }
-
- // Reviewer-by-email.
- if (source.get(ChangeField.REVIEWER_BY_EMAIL.getName()) != null) {
- cd.setReviewersByEmail(
- ChangeField.parseReviewerByEmailFieldValues(
- cd.getId(),
- FluentIterable.from(
- source.get(ChangeField.REVIEWER_BY_EMAIL.getName()).getAsJsonArray())
- .transform(JsonElement::getAsString)));
- } else if (fields.contains(ChangeField.REVIEWER_BY_EMAIL.getName())) {
- cd.setReviewersByEmail(ReviewerByEmailSet.empty());
- }
-
- // Pending-reviewer.
- if (source.get(ChangeField.PENDING_REVIEWER.getName()) != null) {
- cd.setPendingReviewers(
- ChangeField.parseReviewerFieldValues(
- cd.getId(),
- FluentIterable.from(
- source.get(ChangeField.PENDING_REVIEWER.getName()).getAsJsonArray())
- .transform(JsonElement::getAsString)));
- } else if (fields.contains(ChangeField.PENDING_REVIEWER.getName())) {
- cd.setPendingReviewers(ReviewerSet.empty());
- }
-
- // Pending-reviewer-by-email.
- if (source.get(ChangeField.PENDING_REVIEWER_BY_EMAIL.getName()) != null) {
- cd.setPendingReviewersByEmail(
- ChangeField.parseReviewerByEmailFieldValues(
- cd.getId(),
- FluentIterable.from(
- source.get(ChangeField.PENDING_REVIEWER_BY_EMAIL.getName()).getAsJsonArray())
- .transform(JsonElement::getAsString)));
- } else if (fields.contains(ChangeField.PENDING_REVIEWER_BY_EMAIL.getName())) {
- cd.setPendingReviewersByEmail(ReviewerByEmailSet.empty());
- }
-
- // Stored-submit-record-strict.
- decodeSubmitRecords(
- source,
- ChangeField.STORED_SUBMIT_RECORD_STRICT.getName(),
- ChangeField.SUBMIT_RULE_OPTIONS_STRICT,
- cd);
-
- // Stored-submit-record-lenient.
- decodeSubmitRecords(
- source,
- ChangeField.STORED_SUBMIT_RECORD_LENIENT.getName(),
- ChangeField.SUBMIT_RULE_OPTIONS_LENIENT,
- cd);
-
- // Ref-state.
- if (fields.contains(ChangeField.REF_STATE.getName())) {
- cd.setRefStates(RefState.parseStates(getByteArray(source, ChangeField.REF_STATE.getName())));
- }
-
- // Ref-state-pattern.
- if (fields.contains(ChangeField.REF_STATE_PATTERN.getName())) {
- cd.setRefStatePatterns(getByteArray(source, ChangeField.REF_STATE_PATTERN.getName()));
- }
-
- // Unresolved-comment-count.
- decodeUnresolvedCommentCount(source, ChangeField.UNRESOLVED_COMMENT_COUNT.getName(), cd);
-
- // Attention set.
- if (fields.contains(ChangeField.ATTENTION_SET_FULL.getName())) {
- ChangeField.parseAttentionSet(
- FluentIterable.from(source.getAsJsonArray(ChangeField.ATTENTION_SET_FULL.getName()))
- .transform(ElasticChangeIndex::decodeBase64JsonElement)
- .toSet(),
- cd);
- }
-
- if (fields.contains(ChangeField.MERGED_ON.getName())) {
- decodeMergedOn(source, cd);
}
return cd;
}
-
- private Iterable<byte[]> getByteArray(JsonObject source, String name) {
- JsonElement element = source.get(name);
- return element != null
- ? Iterables.transform(element.getAsJsonArray(), e -> decodeBase64(e.getAsString()))
- : Collections.emptyList();
- }
-
- 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(ElasticChangeIndex::decodeBase64JsonElement)
- .toList(),
- opts,
- out);
- }
-
- private static String decodeBase64JsonElement(JsonElement input) {
- return new String(decodeBase64(input.getAsString()), UTF_8);
- }
-
- private void decodeUnresolvedCommentCount(JsonObject doc, String fieldName, ChangeData out) {
- JsonElement count = doc.get(fieldName);
- if (count == null) {
- return;
- }
- out.setUnresolvedCommentCount(count.getAsInt());
- }
-
- private void decodeMergedOn(JsonObject doc, ChangeData out) {
- JsonElement mergedOnField = doc.get(ChangeField.MERGED_ON.getName());
-
- Timestamp mergedOn = null;
- if (mergedOnField != null) {
- // Parse from ElasticMapping.TIMESTAMP_FIELD_FORMAT.
- // We currently use built-in ISO-based dateOptionalTime.
- // https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-date-format.html#built-in-date-formats
- DateTimeFormatter isoFormatter = DateTimeFormatter.ISO_INSTANT;
- mergedOn = Timestamp.from(Instant.from(isoFormatter.parse(mergedOnField.getAsString())));
- }
- out.setMergedOn(mergedOn);
- }
}
diff --git a/java/com/google/gerrit/elasticsearch/ElasticStoredValue.java b/java/com/google/gerrit/elasticsearch/ElasticStoredValue.java
new file mode 100644
index 0000000..a02a715
--- /dev/null
+++ b/java/com/google/gerrit/elasticsearch/ElasticStoredValue.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2021 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.elasticsearch;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.gerrit.index.StoredValue;
+import com.google.gson.JsonElement;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.util.stream.StreamSupport;
+
+/** Bridge to recover fields from the elastic index. */
+public class ElasticStoredValue implements StoredValue {
+ private final JsonElement field;
+
+ ElasticStoredValue(JsonElement field) {
+ this.field = field;
+ }
+
+ @Override
+ public String asString() {
+ return field.getAsString();
+ }
+
+ @Override
+ public Iterable<String> asStrings() {
+ return StreamSupport.stream(field.getAsJsonArray().spliterator(), false)
+ .map(f -> f.getAsString())
+ .collect(toImmutableList());
+ }
+
+ @Override
+ public Integer asInteger() {
+ return field.getAsInt();
+ }
+
+ @Override
+ public Iterable<Integer> asIntegers() {
+ return StreamSupport.stream(field.getAsJsonArray().spliterator(), false)
+ .map(f -> f.getAsInt())
+ .collect(toImmutableList());
+ }
+
+ @Override
+ public Long asLong() {
+ return field.getAsLong();
+ }
+
+ @Override
+ public Iterable<Long> asLongs() {
+ return StreamSupport.stream(field.getAsJsonArray().spliterator(), false)
+ .map(f -> f.getAsLong())
+ .collect(toImmutableList());
+ }
+
+ @Override
+ public Timestamp asTimestamp() {
+ return Timestamp.from(Instant.from(DateTimeFormatter.ISO_INSTANT.parse(field.getAsString())));
+ }
+
+ @Override
+ public byte[] asByteArray() {
+ return AbstractElasticIndex.decodeBase64(field.getAsString());
+ }
+
+ @Override
+ public Iterable<byte[]> asByteArrays() {
+ return StreamSupport.stream(field.getAsJsonArray().spliterator(), false)
+ .map(f -> AbstractElasticIndex.decodeBase64(f.getAsString()))
+ .collect(toImmutableList());
+ }
+}
diff --git a/java/com/google/gerrit/elasticsearch/ElasticVersion.java b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
index 5e72780..6ca51fa 100644
--- a/java/com/google/gerrit/elasticsearch/ElasticVersion.java
+++ b/java/com/google/gerrit/elasticsearch/ElasticVersion.java
@@ -18,7 +18,6 @@
import java.util.regex.Pattern;
public enum ElasticVersion {
- V7_4("7.4.*"),
V7_5("7.5.*"),
V7_6("7.6.*"),
V7_7("7.7.*"),
diff --git a/java/com/google/gerrit/entities/PatchSet.java b/java/com/google/gerrit/entities/PatchSet.java
index 5c8f7eb..b26e5c3 100644
--- a/java/com/google/gerrit/entities/PatchSet.java
+++ b/java/com/google/gerrit/entities/PatchSet.java
@@ -93,6 +93,12 @@
return PatchSet.id(Change.id(changeId), patchSetId);
}
+ /** Parse a PatchSet.Id from an edit ref. */
+ public static PatchSet.Id fromEditRef(String ref) {
+ Change.Id changeId = Change.Id.fromEditRefPart(ref);
+ return PatchSet.id(changeId, Ints.tryParse(ref.substring(ref.lastIndexOf('/') + 1)));
+ }
+
static int fromRef(String ref, int changeIdEnd) {
// Patch set ID.
int ps = changeIdEnd + 1;
diff --git a/java/com/google/gerrit/index/FieldDef.java b/java/com/google/gerrit/index/FieldDef.java
index 63f6887..eb64c1d 100644
--- a/java/com/google/gerrit/index/FieldDef.java
+++ b/java/com/google/gerrit/index/FieldDef.java
@@ -15,6 +15,7 @@
package com.google.gerrit.index;
import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
import com.google.common.base.CharMatcher;
@@ -22,6 +23,7 @@
import com.google.gerrit.exceptions.StorageException;
import java.io.IOException;
import java.sql.Timestamp;
+import java.util.Optional;
/**
* Definition of a field stored in the secondary index.
@@ -65,6 +67,11 @@
T get(I input) throws IOException;
}
+ @FunctionalInterface
+ public interface Setter<I, T> {
+ void set(I object, T value);
+ }
+
public static class Builder<T> {
private final FieldType<T> type;
private final String name;
@@ -81,11 +88,20 @@
}
public <I> FieldDef<I, T> build(Getter<I, T> getter) {
- return new FieldDef<>(name, type, stored, false, getter);
+ return new FieldDef<>(name, type, stored, false, getter, null);
+ }
+
+ public <I> FieldDef<I, T> build(Getter<I, T> getter, Setter<I, T> setter) {
+ return new FieldDef<>(name, type, stored, false, getter, setter);
}
public <I> FieldDef<I, Iterable<T>> buildRepeatable(Getter<I, Iterable<T>> getter) {
- return new FieldDef<>(name, type, stored, true, getter);
+ return new FieldDef<>(name, type, stored, true, getter, null);
+ }
+
+ public <I> FieldDef<I, Iterable<T>> buildRepeatable(
+ Getter<I, Iterable<T>> getter, Setter<I, Iterable<T>> setter) {
+ return new FieldDef<>(name, type, stored, true, getter, setter);
}
}
@@ -96,9 +112,15 @@
private final boolean repeatable;
private final Getter<I, T> getter;
+ private final Optional<Setter<I, T>> setter;
private FieldDef(
- String name, FieldType<?> type, boolean stored, boolean repeatable, Getter<I, T> getter) {
+ String name,
+ FieldType<?> type,
+ boolean stored,
+ boolean repeatable,
+ Getter<I, T> getter,
+ @Nullable Setter<I, T> setter) {
checkArgument(
!(repeatable && type == FieldType.INTEGER_RANGE),
"Range queries against repeated fields are unsupported");
@@ -107,6 +129,7 @@
this.stored = stored;
this.repeatable = repeatable;
this.getter = requireNonNull(getter);
+ this.setter = Optional.ofNullable(setter);
}
private static String checkName(String name) {
@@ -145,6 +168,41 @@
}
}
+ /**
+ * Set the field contents back to an object. Used to reconstruct fields from indexed values. No-op
+ * if the field can't be reconstructed.
+ *
+ * @param object input object.
+ * @param doc indexed document
+ * @return {@code true} if the field was set, {@code false} otherwise
+ */
+ @SuppressWarnings("unchecked")
+ public boolean setIfPossible(I object, StoredValue doc) {
+ if (!setter.isPresent()) {
+ return false;
+ }
+
+ if (FieldType.STRING_TYPES.stream().anyMatch(t -> t.getName().equals(getType().getName()))) {
+ setter.get().set(object, (T) (isRepeatable() ? doc.asStrings() : doc.asString()));
+ return true;
+ } else if (FieldType.INTEGER_TYPES.stream()
+ .anyMatch(t -> t.getName().equals(getType().getName()))) {
+ setter.get().set(object, (T) (isRepeatable() ? doc.asIntegers() : doc.asInteger()));
+ return true;
+ } else if (FieldType.LONG.getName().equals(getType().getName())) {
+ setter.get().set(object, (T) (isRepeatable() ? doc.asLongs() : doc.asLong()));
+ return true;
+ } else if (FieldType.STORED_ONLY.getName().equals(getType().getName())) {
+ setter.get().set(object, (T) (isRepeatable() ? doc.asByteArrays() : doc.asByteArray()));
+ return true;
+ } else if (FieldType.TIMESTAMP.getName().equals(getType().getName())) {
+ checkState(!isRepeatable(), "can't repeat timestamp values");
+ setter.get().set(object, (T) doc.asTimestamp());
+ return true;
+ }
+ return false;
+ }
+
/** @return whether the field is repeatable. */
public boolean isRepeatable() {
return repeatable;
diff --git a/java/com/google/gerrit/index/FieldType.java b/java/com/google/gerrit/index/FieldType.java
index 0db0284..c4c55f23 100644
--- a/java/com/google/gerrit/index/FieldType.java
+++ b/java/com/google/gerrit/index/FieldType.java
@@ -14,6 +14,7 @@
package com.google.gerrit.index;
+import com.google.common.collect.ImmutableList;
import java.sql.Timestamp;
/** Document field types supported by the secondary index system. */
@@ -42,6 +43,14 @@
/** A field that is only stored as raw bytes and cannot be queried. */
public static final FieldType<byte[]> STORED_ONLY = new FieldType<>("STORED_ONLY");
+ /** List of all types that are stored as {@link String} in the index. */
+ public static final ImmutableList<FieldType<String>> STRING_TYPES =
+ ImmutableList.of(EXACT, PREFIX, FULL_TEXT);
+
+ /** List of all types that are stored as {@link Integer} in the index. */
+ public static final ImmutableList<FieldType<Integer>> INTEGER_TYPES =
+ ImmutableList.of(INTEGER_RANGE, INTEGER);
+
private final String name;
private FieldType(String name) {
diff --git a/java/com/google/gerrit/index/IndexType.java b/java/com/google/gerrit/index/IndexType.java
index ee44deb..cade439 100644
--- a/java/com/google/gerrit/index/IndexType.java
+++ b/java/com/google/gerrit/index/IndexType.java
@@ -20,7 +20,8 @@
/**
* Index types supported by the secondary index.
*
- * <p>The explicitly known index types are Lucene (the default) and Elasticsearch.
+ * <p>The explicitly known index types are Lucene (the default), Elasticsearch and a fake index used
+ * in tests.
*
* <p>The third supported index type is any other type String value, deemed as custom. This is for
* configuring index types that are internal or not to be disclosed. Supporting custom index types
@@ -29,6 +30,7 @@
public class IndexType {
private static final String LUCENE = "lucene";
private static final String ELASTICSEARCH = "elasticsearch";
+ private static final String FAKE = "fake";
private final String type;
@@ -41,7 +43,7 @@
}
public static ImmutableSet<String> getKnownTypes() {
- return ImmutableSet.of(LUCENE, ELASTICSEARCH);
+ return ImmutableSet.of(LUCENE, ELASTICSEARCH, FAKE);
}
public boolean isLucene() {
@@ -52,6 +54,10 @@
return type.equals(ELASTICSEARCH);
}
+ public boolean isFake() {
+ return type.equals(FAKE);
+ }
+
@Override
public String toString() {
return type;
diff --git a/java/com/google/gerrit/index/StoredValue.java b/java/com/google/gerrit/index/StoredValue.java
new file mode 100644
index 0000000..fe790c5
--- /dev/null
+++ b/java/com/google/gerrit/index/StoredValue.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2021 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.index;
+
+import java.sql.Timestamp;
+
+/**
+ * Representation of a field stored on the index. Used to load field values from different index
+ * backends.
+ */
+public interface StoredValue {
+ /** Returns the {@link String} value of the field. */
+ String asString();
+
+ /** Returns the {@link String} values of the field. */
+ Iterable<String> asStrings();
+
+ /** Returns the {@link Integer} value of the field. */
+ Integer asInteger();
+
+ /** Returns the {@link Integer} values of the field. */
+ Iterable<Integer> asIntegers();
+
+ /** Returns the {@link Long} value of the field. */
+ Long asLong();
+
+ /** Returns the {@link Long} values of the field. */
+ Iterable<Long> asLongs();
+
+ /** Returns the {@link Timestamp} value of the field. */
+ Timestamp asTimestamp();
+
+ /** Returns the {@code byte[]} value of the field. */
+ byte[] asByteArray();
+
+ /** Returns the {@code byte[]} values of the field. */
+ Iterable<byte[]> asByteArrays();
+}
diff --git a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
new file mode 100644
index 0000000..5cc8e3c
--- /dev/null
+++ b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
@@ -0,0 +1,319 @@
+// Copyright (C) 2021 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.index.testing;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableListMultimap;
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.InternalGroup;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.QueryOptions;
+import com.google.gerrit.index.Schema;
+import com.google.gerrit.index.project.ProjectData;
+import com.google.gerrit.index.project.ProjectIndex;
+import com.google.gerrit.index.query.DataSource;
+import com.google.gerrit.index.query.FieldBundle;
+import com.google.gerrit.index.query.ListResultSet;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.index.query.ResultSet;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.IndexUtils;
+import com.google.gerrit.server.index.account.AccountIndex;
+import com.google.gerrit.server.index.change.ChangeField;
+import com.google.gerrit.server.index.change.ChangeIndex;
+import com.google.gerrit.server.index.group.GroupIndex;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+import java.sql.Timestamp;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Fake secondary index implementation for usage in tests. All values are kept in-memory.
+ *
+ * <p>This class is thread-safe.
+ */
+public abstract class AbstractFakeIndex<K, V, D> implements Index<K, V> {
+ private final Schema<V> schema;
+ /**
+ * SitePaths (config files) are used to signal that an index is ready. This implementation is
+ * consistent with other index backends.
+ */
+ private final SitePaths sitePaths;
+
+ private final String indexName;
+ private final Map<K, D> indexedDocuments;
+
+ AbstractFakeIndex(Schema<V> schema, SitePaths sitePaths, String indexName) {
+ this.schema = schema;
+ this.sitePaths = sitePaths;
+ this.indexName = indexName;
+ this.indexedDocuments = new HashMap<>();
+ }
+
+ @Override
+ public Schema<V> getSchema() {
+ return schema;
+ }
+
+ @Override
+ public void close() {
+ // No-op
+ }
+
+ @Override
+ public void replace(V doc) {
+ synchronized (indexedDocuments) {
+ indexedDocuments.put(keyFor(doc), docFor(doc));
+ }
+ }
+
+ @Override
+ public void delete(K key) {
+ synchronized (indexedDocuments) {
+ indexedDocuments.remove(key);
+ }
+ }
+
+ @Override
+ public void deleteAll() {
+ synchronized (indexedDocuments) {
+ indexedDocuments.clear();
+ }
+ }
+
+ @Override
+ public DataSource<V> getSource(Predicate<V> p, QueryOptions opts) {
+ List<V> results;
+ synchronized (indexedDocuments) {
+ results =
+ indexedDocuments.values().stream()
+ .map(doc -> valueFor(doc))
+ .filter(doc -> p.asMatchable().match(doc))
+ .sorted(sortingComparator())
+ .skip(opts.start())
+ .limit(opts.limit())
+ .collect(toImmutableList());
+ }
+ return new DataSource<V>() {
+ @Override
+ public int getCardinality() {
+ return results.size();
+ }
+
+ @Override
+ public ResultSet<V> read() {
+ return new ListResultSet<>(results);
+ }
+
+ @Override
+ public ResultSet<FieldBundle> readRaw() {
+ ImmutableList.Builder<FieldBundle> fieldBundles = ImmutableList.builder();
+ for (V result : results) {
+ ImmutableListMultimap.Builder<String, Object> fields = ImmutableListMultimap.builder();
+ for (FieldDef<V, ?> field : getSchema().getFields().values()) {
+ if (field.get(result) == null) {
+ continue;
+ }
+ if (field.isRepeatable()) {
+ fields.putAll(field.getName(), (Iterable<?>) field.get(result));
+ } else {
+ fields.put(field.getName(), field.get(result));
+ }
+ }
+ fieldBundles.add(new FieldBundle(fields.build()));
+ }
+ return new ListResultSet<>(fieldBundles.build());
+ }
+ };
+ }
+
+ @Override
+ public void markReady(boolean ready) {
+ IndexUtils.setReady(sitePaths, indexName, schema.getVersion(), ready);
+ }
+
+ /** Method to get a key from a document. */
+ protected abstract K keyFor(V doc);
+
+ /** Method to get a document the index should hold on to from a Gerrit Java data type. */
+ protected abstract D docFor(V value);
+
+ /** Method to a Gerrit Java data type from a document that the index was holding on to. */
+ protected abstract V valueFor(D doc);
+
+ /** Comparator representing the default search order. */
+ protected abstract Comparator<V> sortingComparator();
+
+ /**
+ * Fake implementation of {@link ChangeIndex} where all filtering happens in-memory.
+ *
+ * <p>This index is special in that ChangeData is a mutable object. Therefore we can't just hold
+ * onto the object that the caller wanted us to index. We also can't just create a new ChangeData
+ * from scratch because there are tests that assert that certain computations (e.g. diffs) are
+ * only done once. So we do what the prod indices do: We read and write fields using FieldDef.
+ */
+ public static class FakeChangeIndex
+ extends AbstractFakeIndex<Change.Id, ChangeData, Map<String, Object>> implements ChangeIndex {
+ private final ChangeData.Factory changeDataFactory;
+
+ @Inject
+ FakeChangeIndex(
+ SitePaths sitePaths,
+ ChangeData.Factory changeDataFactory,
+ @Assisted Schema<ChangeData> schema) {
+ super(schema, sitePaths, "changes");
+ this.changeDataFactory = changeDataFactory;
+ }
+
+ @Override
+ protected Change.Id keyFor(ChangeData value) {
+ return value.getId();
+ }
+
+ @Override
+ protected Comparator<ChangeData> sortingComparator() {
+ Comparator<ChangeData> lastUpdated =
+ Comparator.comparing(cd -> cd.change().getLastUpdatedOn());
+ Comparator<ChangeData> merged =
+ Comparator.comparing(cd -> cd.getMergedOn().orElse(new Timestamp(0)));
+ Comparator<ChangeData> id = Comparator.comparing(cd -> cd.getId().get());
+ return lastUpdated.thenComparing(merged).thenComparing(id).reversed();
+ }
+
+ @Override
+ protected Map<String, Object> docFor(ChangeData value) {
+ ImmutableMap.Builder<String, Object> doc = ImmutableMap.builder();
+ for (FieldDef<ChangeData, ?> field : getSchema().getFields().values()) {
+ Object docifiedValue = field.get(value);
+ if (docifiedValue != null) {
+ doc.put(field.getName(), field.get(value));
+ }
+ }
+ return doc.build();
+ }
+
+ @Override
+ protected ChangeData valueFor(Map<String, Object> doc) {
+ ChangeData cd =
+ changeDataFactory.create(
+ Project.nameKey((String) doc.get(ChangeField.PROJECT.getName())),
+ Change.id(Integer.valueOf((String) doc.get(ChangeField.LEGACY_ID_STR.getName()))));
+ for (FieldDef<ChangeData, ?> field : getSchema().getFields().values()) {
+ field.setIfPossible(cd, new FakeStoredValue(doc.get(field.getName())));
+ }
+ return cd;
+ }
+ }
+
+ /** Fake implementation of {@link AccountIndex} where all filtering happens in-memory. */
+ public static class FakeAccountIndex
+ extends AbstractFakeIndex<Account.Id, AccountState, AccountState> implements AccountIndex {
+ @Inject
+ FakeAccountIndex(SitePaths sitePaths, @Assisted Schema<AccountState> schema) {
+ super(schema, sitePaths, "accounts");
+ }
+
+ @Override
+ protected Account.Id keyFor(AccountState value) {
+ return value.account().id();
+ }
+
+ @Override
+ protected AccountState docFor(AccountState value) {
+ return value;
+ }
+
+ @Override
+ protected AccountState valueFor(AccountState doc) {
+ return doc;
+ }
+
+ @Override
+ protected Comparator<AccountState> sortingComparator() {
+ return Comparator.comparing(a -> a.account().id().get());
+ }
+ }
+
+ /** Fake implementation of {@link GroupIndex} where all filtering happens in-memory. */
+ public static class FakeGroupIndex
+ extends AbstractFakeIndex<AccountGroup.UUID, InternalGroup, InternalGroup>
+ implements GroupIndex {
+ @Inject
+ FakeGroupIndex(SitePaths sitePaths, @Assisted Schema<InternalGroup> schema) {
+ super(schema, sitePaths, "groups");
+ }
+
+ @Override
+ protected AccountGroup.UUID keyFor(InternalGroup value) {
+ return value.getGroupUUID();
+ }
+
+ @Override
+ protected InternalGroup docFor(InternalGroup value) {
+ return value;
+ }
+
+ @Override
+ protected InternalGroup valueFor(InternalGroup doc) {
+ return doc;
+ }
+
+ @Override
+ protected Comparator<InternalGroup> sortingComparator() {
+ return Comparator.comparing(g -> g.getId().get());
+ }
+ }
+
+ /** Fake implementation of {@link ProjectIndex} where all filtering happens in-memory. */
+ public static class FakeProjectIndex
+ extends AbstractFakeIndex<Project.NameKey, ProjectData, ProjectData> implements ProjectIndex {
+ @Inject
+ FakeProjectIndex(SitePaths sitePaths, @Assisted Schema<ProjectData> schema) {
+ super(schema, sitePaths, "projects");
+ }
+
+ @Override
+ protected Project.NameKey keyFor(ProjectData value) {
+ return value.getProject().getNameKey();
+ }
+
+ @Override
+ protected ProjectData docFor(ProjectData value) {
+ return value;
+ }
+
+ @Override
+ protected ProjectData valueFor(ProjectData doc) {
+ return doc;
+ }
+
+ @Override
+ protected Comparator<ProjectData> sortingComparator() {
+ return Comparator.comparing(p -> p.getProject().getName());
+ }
+ }
+}
diff --git a/java/com/google/gerrit/index/testing/BUILD b/java/com/google/gerrit/index/testing/BUILD
new file mode 100644
index 0000000..a30eaca
--- /dev/null
+++ b/java/com/google/gerrit/index/testing/BUILD
@@ -0,0 +1,27 @@
+load("@rules_java//java:defs.bzl", "java_library")
+
+java_library(
+ name = "testing",
+ testonly = True,
+ srcs = glob(
+ ["**/*.java"],
+ ),
+ visibility = ["//visibility:public"],
+ deps = [
+ "//java/com/google/gerrit/common:annotations",
+ "//java/com/google/gerrit/entities",
+ "//java/com/google/gerrit/exceptions",
+ "//java/com/google/gerrit/index",
+ "//java/com/google/gerrit/index:query_exception",
+ "//java/com/google/gerrit/index/project",
+ "//java/com/google/gerrit/proto",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/server/logging",
+ "//lib:guava",
+ "//lib:jgit",
+ "//lib:protobuf",
+ "//lib/flogger:api",
+ "//lib/guice",
+ "//lib/guice:guice-assistedinject",
+ ],
+)
diff --git a/java/com/google/gerrit/index/testing/FakeIndexModule.java b/java/com/google/gerrit/index/testing/FakeIndexModule.java
new file mode 100644
index 0000000..126ff10
--- /dev/null
+++ b/java/com/google/gerrit/index/testing/FakeIndexModule.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2021 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.index.testing;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.index.project.ProjectIndex;
+import com.google.gerrit.server.index.AbstractIndexModule;
+import com.google.gerrit.server.index.VersionManager;
+import com.google.gerrit.server.index.account.AccountIndex;
+import com.google.gerrit.server.index.change.ChangeIndex;
+import com.google.gerrit.server.index.group.GroupIndex;
+import java.util.Map;
+
+/** Module to bind {@link FakeIndexModule}. */
+public class FakeIndexModule extends AbstractIndexModule {
+ public static FakeIndexModule singleVersionAllLatest(int threads, boolean secondary) {
+ return new FakeIndexModule(ImmutableMap.of(), threads, secondary);
+ }
+
+ public static FakeIndexModule singleVersionWithExplicitVersions(
+ Map<String, Integer> versions, int threads, boolean secondary) {
+ return new FakeIndexModule(versions, threads, secondary);
+ }
+
+ public static FakeIndexModule latestVersion(boolean secondary) {
+ return new FakeIndexModule(null, -1 /* direct executor */, secondary);
+ }
+
+ private FakeIndexModule(Map<String, Integer> singleVersions, int threads, boolean secondary) {
+ super(singleVersions, threads, secondary);
+ }
+
+ @Override
+ protected Class<? extends AccountIndex> getAccountIndex() {
+ return AbstractFakeIndex.FakeAccountIndex.class;
+ }
+
+ @Override
+ protected Class<? extends ChangeIndex> getChangeIndex() {
+ return AbstractFakeIndex.FakeChangeIndex.class;
+ }
+
+ @Override
+ protected Class<? extends GroupIndex> getGroupIndex() {
+ return AbstractFakeIndex.FakeGroupIndex.class;
+ }
+
+ @Override
+ protected Class<? extends ProjectIndex> getProjectIndex() {
+ return AbstractFakeIndex.FakeProjectIndex.class;
+ }
+
+ @Override
+ protected Class<? extends VersionManager> getVersionManager() {
+ return FakeIndexVersionManager.class;
+ }
+}
diff --git a/java/com/google/gerrit/index/testing/FakeIndexVersionManager.java b/java/com/google/gerrit/index/testing/FakeIndexVersionManager.java
new file mode 100644
index 0000000..5044e38
--- /dev/null
+++ b/java/com/google/gerrit/index/testing/FakeIndexVersionManager.java
@@ -0,0 +1,75 @@
+// Copyright (C) 2021 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.index.testing;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.index.Index;
+import com.google.gerrit.index.IndexDefinition;
+import com.google.gerrit.index.Schema;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.index.GerritIndexStatus;
+import com.google.gerrit.server.index.OnlineUpgradeListener;
+import com.google.gerrit.server.index.VersionManager;
+import com.google.gerrit.server.plugincontext.PluginSetContext;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.util.Collection;
+import java.util.TreeMap;
+import org.eclipse.jgit.lib.Config;
+
+/** Fake version manager for {@link AbstractFakeIndex}. */
+@Singleton
+public class FakeIndexVersionManager extends VersionManager {
+
+ @Inject
+ FakeIndexVersionManager(
+ @GerritServerConfig Config cfg,
+ SitePaths sitePaths,
+ PluginSetContext<OnlineUpgradeListener> listeners,
+ Collection<IndexDefinition<?, ?, ?>> defs) {
+ super(sitePaths, listeners, defs, VersionManager.getOnlineUpgrade(cfg));
+ }
+
+ @Override
+ protected <K, V, I extends Index<K, V>> TreeMap<Integer, Version<V>> scanVersions(
+ IndexDefinition<K, V, I> def, GerritIndexStatus cfg) {
+ TreeMap<Integer, Version<V>> versions = new TreeMap<>();
+ for (Schema<V> schema : def.getSchemas().values()) {
+ int v = schema.getVersion();
+ boolean exists = versions.containsKey(v);
+ versions.put(v, new Version<>(schema, v, exists, cfg.getReady(def.getName(), v)));
+ }
+ return versions;
+ }
+
+ @Override
+ protected <K, V, I extends Index<K, V>> void initIndex(
+ IndexDefinition<K, V, I> def, GerritIndexStatus cfg) {
+ // Set latest versions ready.
+ if (def.getSchemas().isEmpty()) {
+ super.initIndex(def, cfg);
+ return;
+ }
+ Schema<V> schema = Iterables.getLast(def.getSchemas().values());
+ try {
+ cfg.setReady(def.getName(), schema.getVersion(), true);
+ cfg.save();
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ super.initIndex(def, cfg);
+ }
+}
diff --git a/java/com/google/gerrit/index/testing/FakeStoredValue.java b/java/com/google/gerrit/index/testing/FakeStoredValue.java
new file mode 100644
index 0000000..ca46ed1
--- /dev/null
+++ b/java/com/google/gerrit/index/testing/FakeStoredValue.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2021 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.index.testing;
+
+import com.google.gerrit.index.StoredValue;
+import java.sql.Timestamp;
+
+/** Bridge to recover fields from the fake index. */
+public class FakeStoredValue implements StoredValue {
+ private final Object field;
+
+ FakeStoredValue(Object field) {
+ this.field = field;
+ }
+
+ @Override
+ public String asString() {
+ return (String) field;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Iterable<String> asStrings() {
+ return (Iterable<String>) field;
+ }
+
+ @Override
+ public Integer asInteger() {
+ return (Integer) field;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Iterable<Integer> asIntegers() {
+ return (Iterable<Integer>) field;
+ }
+
+ @Override
+ public Long asLong() {
+ return (Long) field;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Iterable<Long> asLongs() {
+ return (Iterable<Long>) field;
+ }
+
+ @Override
+ public Timestamp asTimestamp() {
+ return (Timestamp) field;
+ }
+
+ @Override
+ public byte[] asByteArray() {
+ return (byte[]) field;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Iterable<byte[]> asByteArrays() {
+ return (Iterable<byte[]>) field;
+ }
+}
diff --git a/java/com/google/gerrit/lucene/LuceneChangeIndex.java b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
index c3d4440..ac616ca 100644
--- a/java/com/google/gerrit/lucene/LuceneChangeIndex.java
+++ b/java/com/google/gerrit/lucene/LuceneChangeIndex.java
@@ -15,7 +15,6 @@
package com.google.gerrit.lucene;
import static com.google.common.collect.ImmutableList.toImmutableList;
-import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.lucene.AbstractLuceneIndex.sortFieldName;
import static com.google.gerrit.server.git.QueueProvider.QueueType.INTERACTIVE;
import static com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
@@ -24,11 +23,8 @@
import static com.google.gerrit.server.index.change.ChangeIndexRewriter.CLOSED_STATUSES;
import static com.google.gerrit.server.index.change.ChangeIndexRewriter.OPEN_STATUSES;
import static java.util.Objects.requireNonNull;
-import static java.util.stream.Collectors.toList;
import com.google.common.base.Throwables;
-import com.google.common.collect.Collections2;
-import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
@@ -38,25 +34,19 @@
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
-import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.converter.ChangeProtoConverter;
-import com.google.gerrit.entities.converter.PatchSetApprovalProtoConverter;
-import com.google.gerrit.entities.converter.PatchSetProtoConverter;
import com.google.gerrit.entities.converter.ProtoConverter;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.QueryOptions;
-import com.google.gerrit.index.RefState;
import com.google.gerrit.index.Schema;
import com.google.gerrit.index.query.FieldBundle;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.index.query.QueryParseException;
import com.google.gerrit.index.query.ResultSet;
import com.google.gerrit.proto.Protos;
-import com.google.gerrit.server.StarredChangesUtil;
import com.google.gerrit.server.change.MergeabilityComputationBehavior;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
@@ -65,7 +55,6 @@
import com.google.gerrit.server.index.change.ChangeField;
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.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeDataSource;
import com.google.inject.Inject;
@@ -73,16 +62,13 @@
import com.google.protobuf.MessageLite;
import java.io.IOException;
import java.nio.file.Path;
-import java.sql.Timestamp;
import java.util.ArrayList;
-import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
-import java.util.function.Consumer;
import java.util.function.Function;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.IndexWriter;
@@ -119,34 +105,10 @@
private static final String CHANGES = "changes";
private static final String CHANGES_OPEN = "open";
private static final String CHANGES_CLOSED = "closed";
- private static final String ADDED_FIELD = ChangeField.ADDED.getName();
- private static final String APPROVAL_FIELD = ChangeField.APPROVAL.getName();
private static final String CHANGE_FIELD = ChangeField.CHANGE.getName();
- private static final String DELETED_FIELD = ChangeField.DELETED.getName();
- private static final String MERGEABLE_FIELD = ChangeField.MERGEABLE.getName();
- private static final String PATCH_SET_FIELD = ChangeField.PATCH_SET.getName();
- private static final String PENDING_REVIEWER_FIELD = ChangeField.PENDING_REVIEWER.getName();
- private static final String PENDING_REVIEWER_BY_EMAIL_FIELD =
- ChangeField.PENDING_REVIEWER_BY_EMAIL.getName();
- private static final String REF_STATE_FIELD = ChangeField.REF_STATE.getName();
- private static final String REF_STATE_PATTERN_FIELD = ChangeField.REF_STATE_PATTERN.getName();
- private static final String REVIEWEDBY_FIELD = ChangeField.REVIEWEDBY.getName();
- private static final String REVIEWER_FIELD = ChangeField.REVIEWER.getName();
- private static final String REVIEWER_BY_EMAIL_FIELD = ChangeField.REVIEWER_BY_EMAIL.getName();
- 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();
- private static final String TOTAL_COMMENT_COUNT_FIELD = ChangeField.TOTAL_COMMENT_COUNT.getName();
- private static final String UNRESOLVED_COMMENT_COUNT_FIELD =
- ChangeField.UNRESOLVED_COMMENT_COUNT.getName();
- private static final String ATTENTION_SET_FULL_FIELD = ChangeField.ATTENTION_SET_FULL.getName();
- private static final String MERGED_ON_FIELD = ChangeField.MERGED_ON.getName();
@FunctionalInterface
- static interface IdTerm {
+ interface IdTerm {
Term get(String name, int id);
}
@@ -159,7 +121,7 @@
}
@FunctionalInterface
- static interface ChangeIdExtractor {
+ interface ChangeIdExtractor {
Change.Id extract(IndexableField f);
}
@@ -520,231 +482,14 @@
cd = changeDataFactory.create(Project.nameKey(project.stringValue()), extractor.extract(f));
}
- // Any decoding that is done here must also be done in {@link ElasticChangeIndex}.
-
- if (fields.contains(PATCH_SET_FIELD)) {
- decodePatchSets(doc, cd);
+ for (FieldDef<ChangeData, ?> field : getSchema().getFields().values()) {
+ if (fields.contains(field.getName()) && doc.get(field.getName()) != null) {
+ field.setIfPossible(cd, new LuceneStoredValue(doc.get(field.getName())));
+ }
}
- if (fields.contains(APPROVAL_FIELD)) {
- decodeApprovals(doc, cd);
- }
- if (fields.contains(ADDED_FIELD) && fields.contains(DELETED_FIELD)) {
- decodeChangedLines(doc, cd);
- }
- if (fields.contains(MERGEABLE_FIELD)) {
- decodeMergeable(doc, cd);
- }
- if (fields.contains(REVIEWEDBY_FIELD)) {
- decodeReviewedBy(doc, cd);
- }
- if (fields.contains(HASHTAG_FIELD)) {
- decodeHashtags(doc, cd);
- }
- if (fields.contains(STAR_FIELD)) {
- decodeStar(doc, cd);
- }
- if (fields.contains(REVIEWER_FIELD)) {
- decodeReviewers(doc, cd);
- }
- if (fields.contains(REVIEWER_BY_EMAIL_FIELD)) {
- decodeReviewersByEmail(doc, cd);
- }
- if (fields.contains(PENDING_REVIEWER_FIELD)) {
- decodePendingReviewers(doc, cd);
- }
- if (fields.contains(PENDING_REVIEWER_BY_EMAIL_FIELD)) {
- decodePendingReviewersByEmail(doc, cd);
- }
- if (fields.contains(ATTENTION_SET_FULL_FIELD)) {
- decodeAttentionSet(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);
- if (fields.contains(REF_STATE_FIELD)) {
- decodeRefStates(doc, cd);
- }
- if (fields.contains(REF_STATE_PATTERN_FIELD)) {
- decodeRefStatePatterns(doc, cd);
- }
- if (fields.contains(MERGED_ON_FIELD)) {
- decodeMergedOn(doc, cd);
- }
-
- decodeUnresolvedCommentCount(doc, cd);
- decodeTotalCommentCount(doc, cd);
return cd;
}
- private void decodePatchSets(ListMultimap<String, IndexableField> doc, ChangeData cd) {
- List<PatchSet> patchSets = decodeProtos(doc, PATCH_SET_FIELD, PatchSetProtoConverter.INSTANCE);
- if (!patchSets.isEmpty()) {
- // Will be an empty list for schemas prior to when this field was stored;
- // this cannot be valid since a change needs at least one patch set.
- cd.setPatchSets(patchSets);
- }
- }
-
- private void decodeApprovals(ListMultimap<String, IndexableField> doc, ChangeData cd) {
- cd.setCurrentApprovals(
- decodeProtos(doc, APPROVAL_FIELD, PatchSetApprovalProtoConverter.INSTANCE));
- }
-
- private void decodeChangedLines(ListMultimap<String, IndexableField> doc, ChangeData cd) {
- IndexableField added = Iterables.getFirst(doc.get(ADDED_FIELD), null);
- IndexableField deleted = Iterables.getFirst(doc.get(DELETED_FIELD), null);
- if (added != null && deleted != null) {
- cd.setChangedLines(added.numericValue().intValue(), deleted.numericValue().intValue());
- } else {
- // No ChangedLines stored, likely due to failure during reindexing, for
- // example due to LargeObjectException. But we know the field was
- // requested, so update ChangeData to prevent callers from trying to
- // lazily load it, as that would probably also fail.
- cd.setNoChangedLines();
- }
- }
-
- private void decodeMergeable(ListMultimap<String, IndexableField> doc, ChangeData cd) {
- IndexableField f = Iterables.getFirst(doc.get(MERGEABLE_FIELD), null);
- if (f != null && !skipFields.contains(MERGEABLE_FIELD)) {
- String mergeable = f.stringValue();
- if ("1".equals(mergeable)) {
- cd.setMergeable(true);
- } else if ("0".equals(mergeable)) {
- cd.setMergeable(false);
- }
- }
- }
-
- private void decodeReviewedBy(ListMultimap<String, IndexableField> doc, ChangeData cd) {
- Collection<IndexableField> reviewedBy = doc.get(REVIEWEDBY_FIELD);
- if (!reviewedBy.isEmpty()) {
- Set<Account.Id> accounts = Sets.newHashSetWithExpectedSize(reviewedBy.size());
- for (IndexableField r : reviewedBy) {
- int id = r.numericValue().intValue();
- if (reviewedBy.size() == 1 && id == ChangeField.NOT_REVIEWED) {
- break;
- }
- accounts.add(Account.id(id));
- }
- cd.setReviewedBy(accounts);
- }
- }
-
- private void decodeHashtags(ListMultimap<String, IndexableField> doc, ChangeData cd) {
- Collection<IndexableField> hashtag = doc.get(HASHTAG_FIELD);
- Set<String> hashtags = Sets.newHashSetWithExpectedSize(hashtag.size());
- for (IndexableField r : hashtag) {
- hashtags.add(r.binaryValue().utf8ToString());
- }
- cd.setHashtags(hashtags);
- }
-
- private void decodeStar(ListMultimap<String, IndexableField> doc, ChangeData cd) {
- Collection<IndexableField> star = doc.get(STAR_FIELD);
- ListMultimap<Account.Id, String> stars = MultimapBuilder.hashKeys().arrayListValues().build();
- for (IndexableField r : star) {
- StarredChangesUtil.StarField starField = StarredChangesUtil.StarField.parse(r.stringValue());
- if (starField != null) {
- stars.put(starField.accountId(), starField.label());
- }
- }
- cd.setStars(stars);
- }
-
- private void decodeReviewers(ListMultimap<String, IndexableField> doc, ChangeData cd) {
- cd.setReviewers(
- ChangeField.parseReviewerFieldValues(
- cd.getId(),
- FluentIterable.from(doc.get(REVIEWER_FIELD)).transform(IndexableField::stringValue)));
- }
-
- private void decodeReviewersByEmail(ListMultimap<String, IndexableField> doc, ChangeData cd) {
- cd.setReviewersByEmail(
- ChangeField.parseReviewerByEmailFieldValues(
- cd.getId(),
- FluentIterable.from(doc.get(REVIEWER_BY_EMAIL_FIELD))
- .transform(IndexableField::stringValue)));
- }
-
- private void decodePendingReviewers(ListMultimap<String, IndexableField> doc, ChangeData cd) {
- cd.setPendingReviewers(
- ChangeField.parseReviewerFieldValues(
- cd.getId(),
- FluentIterable.from(doc.get(PENDING_REVIEWER_FIELD))
- .transform(IndexableField::stringValue)));
- }
-
- private void decodePendingReviewersByEmail(
- ListMultimap<String, IndexableField> doc, ChangeData cd) {
- cd.setPendingReviewersByEmail(
- ChangeField.parseReviewerByEmailFieldValues(
- cd.getId(),
- FluentIterable.from(doc.get(PENDING_REVIEWER_BY_EMAIL_FIELD))
- .transform(IndexableField::stringValue)));
- }
-
- private void decodeAttentionSet(ListMultimap<String, IndexableField> doc, ChangeData cd) {
- ChangeField.parseAttentionSet(
- doc.get(ATTENTION_SET_FULL_FIELD).stream()
- .map(field -> field.binaryValue().utf8ToString())
- .collect(toImmutableSet()),
- cd);
- }
-
- private void decodeSubmitRecords(
- ListMultimap<String, IndexableField> doc,
- String field,
- SubmitRuleOptions opts,
- ChangeData cd) {
- ChangeField.parseSubmitRecords(
- Collections2.transform(doc.get(field), f -> f.binaryValue().utf8ToString()), opts, cd);
- }
-
- private void decodeRefStates(ListMultimap<String, IndexableField> doc, ChangeData cd) {
- cd.setRefStates(RefState.parseStates(copyAsBytes(doc.get(REF_STATE_FIELD))));
- }
-
- private void decodeRefStatePatterns(ListMultimap<String, IndexableField> doc, ChangeData cd) {
- cd.setRefStatePatterns(copyAsBytes(doc.get(REF_STATE_PATTERN_FIELD)));
- }
-
- private void decodeUnresolvedCommentCount(
- ListMultimap<String, IndexableField> doc, ChangeData cd) {
- decodeIntField(doc, UNRESOLVED_COMMENT_COUNT_FIELD, cd::setUnresolvedCommentCount);
- }
-
- private void decodeTotalCommentCount(ListMultimap<String, IndexableField> doc, ChangeData cd) {
- decodeIntField(doc, TOTAL_COMMENT_COUNT_FIELD, cd::setTotalCommentCount);
- }
-
- private static void decodeIntField(
- ListMultimap<String, IndexableField> doc, String fieldName, Consumer<Integer> consumer) {
- IndexableField f = Iterables.getFirst(doc.get(fieldName), null);
- if (f != null && f.numericValue() != null) {
- consumer.accept(f.numericValue().intValue());
- }
- }
-
- private void decodeMergedOn(ListMultimap<String, IndexableField> doc, ChangeData cd) {
- IndexableField mergedOnField =
- Iterables.getFirst(doc.get(MERGED_ON_FIELD), /* defaultValue= */ null);
- Timestamp mergedOn = null;
- if (mergedOnField != null && mergedOnField.numericValue() != null) {
- mergedOn = new Timestamp(mergedOnField.numericValue().longValue());
- }
- cd.setMergedOn(mergedOn);
- }
-
- private static <T> List<T> decodeProtos(
- ListMultimap<String, IndexableField> doc, String fieldName, ProtoConverter<?, T> converter) {
- return doc.get(fieldName).stream()
- .map(IndexableField::binaryValue)
- .map(bytesRef -> parseProtoFrom(bytesRef, converter))
- .collect(toImmutableList());
- }
-
private static <P extends MessageLite, T> T parseProtoFrom(
BytesRef bytesRef, ProtoConverter<P, T> converter) {
P message =
@@ -752,16 +497,4 @@
converter.getParser(), bytesRef.bytes, bytesRef.offset, bytesRef.length);
return converter.fromProto(message);
}
-
- private static List<byte[]> copyAsBytes(Collection<IndexableField> fields) {
- return fields.stream()
- .map(
- f -> {
- BytesRef ref = f.binaryValue();
- byte[] b = new byte[ref.length];
- System.arraycopy(ref.bytes, ref.offset, b, 0, ref.length);
- return b;
- })
- .collect(toList());
- }
}
diff --git a/java/com/google/gerrit/lucene/LuceneStoredValue.java b/java/com/google/gerrit/lucene/LuceneStoredValue.java
new file mode 100644
index 0000000..efe489b
--- /dev/null
+++ b/java/com/google/gerrit/lucene/LuceneStoredValue.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2021 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.lucene;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.index.StoredValue;
+import java.sql.Timestamp;
+import java.util.List;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.util.BytesRef;
+
+/** Bridge to recover fields from the lucene index. */
+public class LuceneStoredValue implements StoredValue {
+ /**
+ * Lucene represents repeated fields as a list of {@link IndexableField}, so we hold onto a list
+ * here to cover both repeated and non-repeated fields.
+ */
+ private final List<IndexableField> field;
+
+ LuceneStoredValue(List<IndexableField> field) {
+ this.field = field;
+ }
+
+ @Override
+ public String asString() {
+ return Iterables.getFirst(asStrings(), null);
+ }
+
+ @Override
+ public Iterable<String> asStrings() {
+ return field.stream().map(f -> f.stringValue()).collect(toImmutableList());
+ }
+
+ @Override
+ public Integer asInteger() {
+ return Iterables.getFirst(asIntegers(), null);
+ }
+
+ @Override
+ public Iterable<Integer> asIntegers() {
+ return field.stream().map(f -> f.numericValue().intValue()).collect(toImmutableList());
+ }
+
+ @Override
+ public Long asLong() {
+ return Iterables.getFirst(asLongs(), null);
+ }
+
+ @Override
+ public Iterable<Long> asLongs() {
+ return field.stream().map(f -> f.numericValue().longValue()).collect(toImmutableList());
+ }
+
+ @Override
+ public Timestamp asTimestamp() {
+ return asLong() == null ? null : new Timestamp(asLong());
+ }
+
+ @Override
+ public byte[] asByteArray() {
+ return Iterables.getFirst(asByteArrays(), null);
+ }
+
+ @Override
+ public Iterable<byte[]> asByteArrays() {
+ return copyAsBytes(field);
+ }
+
+ private static List<byte[]> copyAsBytes(List<IndexableField> fields) {
+ return fields.stream()
+ .map(
+ f -> {
+ BytesRef ref = f.binaryValue();
+ byte[] b = new byte[ref.length];
+ System.arraycopy(ref.bytes, ref.offset, b, 0, ref.length);
+ return b;
+ })
+ .collect(toList());
+ }
+}
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
index 6c1f097..5e951d3 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommitsAdvertiseRefsHook.java
@@ -27,9 +27,9 @@
import com.google.gerrit.server.git.HookUtil;
import com.google.gerrit.server.index.change.ChangeField;
import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangePredicates;
import com.google.gerrit.server.query.change.ChangeStatusPredicate;
import com.google.gerrit.server.query.change.InternalChangeQuery;
-import com.google.gerrit.server.query.change.OwnerPredicate;
import com.google.gerrit.server.query.change.ProjectPredicate;
import com.google.gerrit.server.util.MagicBranch;
import com.google.inject.Provider;
@@ -118,7 +118,7 @@
Predicate.and(
new ProjectPredicate(projectName.get()),
ChangeStatusPredicate.open(),
- new OwnerPredicate(user)))) {
+ ChangePredicates.owner(user)))) {
PatchSet ps = cd.currentPatchSet();
if (ps != null) {
// Ensure we actually observed a patch set ref pointing to this
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 7e84f1d..7131d44 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -16,6 +16,7 @@
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
+import static com.google.common.collect.ImmutableListMultimap.toImmutableListMultimap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.index.FieldDef.exact;
import static com.google.gerrit.index.FieldDef.fullText;
@@ -57,6 +58,7 @@
import com.google.gerrit.entities.converter.PatchSetProtoConverter;
import com.google.gerrit.entities.converter.ProtoConverter;
import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.RefState;
import com.google.gerrit.index.SchemaUtil;
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.proto.Protos;
@@ -71,6 +73,7 @@
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeStatusPredicate;
import com.google.gson.Gson;
+import com.google.protobuf.MessageLite;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
@@ -84,6 +87,7 @@
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
import org.eclipse.jgit.lib.PersonIdent;
/**
@@ -153,7 +157,7 @@
public static final FieldDef<ChangeData, Timestamp> MERGED_ON =
timestamp(ChangeQueryBuilder.FIELD_MERGED_ON)
.stored()
- .build(cd -> cd.getMergedOn().orElse(null));
+ .build(cd -> cd.getMergedOn().orElse(null), (cd, field) -> cd.setMergedOn(field));
/** List of full file paths modified in the current patch set. */
public static final FieldDef<ChangeData, Iterable<String>> PATH =
@@ -188,7 +192,12 @@
public static final FieldDef<ChangeData, Iterable<byte[]>> HASHTAG_CASE_AWARE =
storedOnly("_hashtag")
.buildRepeatable(
- cd -> cd.hashtags().stream().map(t -> t.getBytes(UTF_8)).collect(toSet()));
+ cd -> cd.hashtags().stream().map(t -> t.getBytes(UTF_8)).collect(toSet()),
+ (cd, field) ->
+ cd.setHashtags(
+ StreamSupport.stream(field.spliterator(), false)
+ .map(f -> new String(f, UTF_8))
+ .collect(toImmutableSet())));
/** Components of each file path modified in the current patch set. */
public static final FieldDef<ChangeData, Iterable<String>> FILE_PART =
@@ -335,7 +344,14 @@
*/
public static final FieldDef<ChangeData, Iterable<byte[]>> ATTENTION_SET_FULL =
storedOnly(ChangeQueryBuilder.FIELD_ATTENTION_SET_FULL)
- .buildRepeatable(ChangeField::storedAttentionSet);
+ .buildRepeatable(
+ ChangeField::storedAttentionSet,
+ (cd, value) ->
+ parseAttentionSet(
+ StreamSupport.stream(value.spliterator(), false)
+ .map(v -> new String(v, UTF_8))
+ .collect(toImmutableSet()),
+ cd));
/** The user assigned to the change. */
public static final FieldDef<ChangeData, Integer> ASSIGNEE =
@@ -344,25 +360,38 @@
/** Reviewer(s) associated with the change. */
public static final FieldDef<ChangeData, Iterable<String>> REVIEWER =
- exact("reviewer2").stored().buildRepeatable(cd -> getReviewerFieldValues(cd.reviewers()));
+ exact("reviewer2")
+ .stored()
+ .buildRepeatable(
+ cd -> getReviewerFieldValues(cd.reviewers()),
+ (cd, field) -> cd.setReviewers(parseReviewerFieldValues(cd.getId(), field)));
/** Reviewer(s) associated with the change that do not have a gerrit account. */
public static final FieldDef<ChangeData, Iterable<String>> REVIEWER_BY_EMAIL =
exact("reviewer_by_email")
.stored()
- .buildRepeatable(cd -> getReviewerByEmailFieldValues(cd.reviewersByEmail()));
+ .buildRepeatable(
+ cd -> getReviewerByEmailFieldValues(cd.reviewersByEmail()),
+ (cd, field) ->
+ cd.setReviewersByEmail(parseReviewerByEmailFieldValues(cd.getId(), field)));
/** Reviewer(s) modified during change's current WIP phase. */
public static final FieldDef<ChangeData, Iterable<String>> PENDING_REVIEWER =
exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER)
.stored()
- .buildRepeatable(cd -> getReviewerFieldValues(cd.pendingReviewers()));
+ .buildRepeatable(
+ cd -> getReviewerFieldValues(cd.pendingReviewers()),
+ (cd, field) -> cd.setPendingReviewers(parseReviewerFieldValues(cd.getId(), field)));
/** Reviewer(s) by email modified during change's current WIP phase. */
public static final FieldDef<ChangeData, Iterable<String>> PENDING_REVIEWER_BY_EMAIL =
exact(ChangeQueryBuilder.FIELD_PENDING_REVIEWER_BY_EMAIL)
.stored()
- .buildRepeatable(cd -> getReviewerByEmailFieldValues(cd.pendingReviewersByEmail()));
+ .buildRepeatable(
+ cd -> getReviewerByEmailFieldValues(cd.pendingReviewersByEmail()),
+ (cd, field) ->
+ cd.setPendingReviewersByEmail(
+ parseReviewerByEmailFieldValues(cd.getId(), field)));
/** References a change that this change reverts. */
public static final FieldDef<ChangeData, Integer> REVERT_OF =
@@ -642,13 +671,18 @@
/** Serialized change object, used for pre-populating results. */
public static final FieldDef<ChangeData, byte[]> CHANGE =
storedOnly("_change")
- .build(changeGetter(change -> toProto(ChangeProtoConverter.INSTANCE, change)));
+ .build(
+ changeGetter(change -> toProto(ChangeProtoConverter.INSTANCE, change)),
+ (cd, field) -> cd.setChange(parseProtoFrom(field, ChangeProtoConverter.INSTANCE)));
/** Serialized approvals for the current patch set, used for pre-populating results. */
public static final FieldDef<ChangeData, Iterable<byte[]>> APPROVAL =
storedOnly("_approval")
.buildRepeatable(
- cd -> toProtos(PatchSetApprovalProtoConverter.INSTANCE, cd.currentApprovals()));
+ cd -> toProtos(PatchSetApprovalProtoConverter.INSTANCE, cd.currentApprovals()),
+ (cd, field) ->
+ cd.setCurrentApprovals(
+ decodeProtos(field, PatchSetApprovalProtoConverter.INSTANCE)));
public static String formatLabel(String label, int value) {
return formatLabel(label, value, null);
@@ -689,11 +723,14 @@
/** Number of unresolved comment threads of the change, including robot comments. */
public static final FieldDef<ChangeData, Integer> UNRESOLVED_COMMENT_COUNT =
intRange(ChangeQueryBuilder.FIELD_UNRESOLVED_COMMENT_COUNT)
- .build(ChangeData::unresolvedCommentCount);
+ .build(
+ ChangeData::unresolvedCommentCount,
+ (cd, field) -> cd.setUnresolvedCommentCount(field));
/** Total number of published inline comments of the change, including robot comments. */
public static final FieldDef<ChangeData, Integer> TOTAL_COMMENT_COUNT =
- intRange("total_comments").build(ChangeData::totalCommentCount);
+ intRange("total_comments")
+ .build(ChangeData::totalCommentCount, (cd, field) -> cd.setTotalCommentCount(field));
/** Whether the change is mergeable. */
public static final FieldDef<ChangeData, String> MERGEABLE =
@@ -706,7 +743,8 @@
return null;
}
return m ? "1" : "0";
- });
+ },
+ (cd, field) -> cd.setMergeable(field == null ? false : field.equals("1")));
/** Whether the change is a merge commit. */
public static final FieldDef<ChangeData, String> MERGE =
@@ -724,12 +762,16 @@
/** The number of inserted lines in this change. */
public static final FieldDef<ChangeData, Integer> ADDED =
intRange(ChangeQueryBuilder.FIELD_ADDED)
- .build(cd -> cd.changedLines().isPresent() ? cd.changedLines().get().insertions : null);
+ .build(
+ cd -> cd.changedLines().isPresent() ? cd.changedLines().get().insertions : null,
+ (cd, field) -> cd.setLinesInserted(field));
/** The number of deleted lines in this change. */
public static final FieldDef<ChangeData, Integer> DELETED =
intRange(ChangeQueryBuilder.FIELD_DELETED)
- .build(cd -> cd.changedLines().isPresent() ? cd.changedLines().get().deletions : null);
+ .build(
+ cd -> cd.changedLines().isPresent() ? cd.changedLines().get().deletions : null,
+ (cd, field) -> cd.setLinesDeleted(field));
/** The total number of modified lines in this change. */
public static final FieldDef<ChangeData, Integer> DELTA =
@@ -770,8 +812,12 @@
Iterables.transform(
cd.stars().entries(),
e ->
- StarredChangesUtil.StarField.create(e.getKey(), e.getValue())
- .toString()));
+ StarredChangesUtil.StarField.create(e.getKey(), e.getValue()).toString()),
+ (cd, field) ->
+ cd.setStars(
+ StreamSupport.stream(field.spliterator(), false)
+ .map(f -> StarredChangesUtil.StarField.parse(f))
+ .collect(toImmutableListMultimap(e -> e.accountId(), e -> e.label()))));
/** Users that have starred the change with any label. */
public static final FieldDef<ChangeData, Iterable<Integer>> STARBY =
@@ -787,7 +833,9 @@
/** Serialized patch set object, used for pre-populating results. */
public static final FieldDef<ChangeData, Iterable<byte[]>> PATCH_SET =
storedOnly("_patch_set")
- .buildRepeatable(cd -> toProtos(PatchSetProtoConverter.INSTANCE, cd.patchSets()));
+ .buildRepeatable(
+ cd -> toProtos(PatchSetProtoConverter.INSTANCE, cd.patchSets()),
+ (cd, field) -> cd.setPatchSets(decodeProtos(field, PatchSetProtoConverter.INSTANCE)));
/** Users who have edits on this change. */
public static final FieldDef<ChangeData, Iterable<Integer>> EDITBY =
@@ -821,7 +869,12 @@
return ImmutableSet.of(NOT_REVIEWED);
}
return reviewedBy.stream().map(Account.Id::get).collect(toList());
- });
+ },
+ (cd, field) ->
+ cd.setReviewedBy(
+ StreamSupport.stream(field.spliterator(), false)
+ .map(Account::id)
+ .collect(toImmutableSet())));
public static final SubmitRuleOptions SUBMIT_RULE_OPTIONS_LENIENT =
SubmitRuleOptions.builder().recomputeOnClosedChanges(true).build();
@@ -917,11 +970,27 @@
public static final FieldDef<ChangeData, Iterable<byte[]>> STORED_SUBMIT_RECORD_STRICT =
storedOnly("full_submit_record_strict")
- .buildRepeatable(cd -> storedSubmitRecords(cd, SUBMIT_RULE_OPTIONS_STRICT));
+ .buildRepeatable(
+ cd -> storedSubmitRecords(cd, SUBMIT_RULE_OPTIONS_STRICT),
+ (cd, field) ->
+ parseSubmitRecords(
+ StreamSupport.stream(field.spliterator(), false)
+ .map(f -> new String(f, UTF_8))
+ .collect(toSet()),
+ SUBMIT_RULE_OPTIONS_STRICT,
+ cd));
public static final FieldDef<ChangeData, Iterable<byte[]>> STORED_SUBMIT_RECORD_LENIENT =
storedOnly("full_submit_record_lenient")
- .buildRepeatable(cd -> storedSubmitRecords(cd, SUBMIT_RULE_OPTIONS_LENIENT));
+ .buildRepeatable(
+ cd -> storedSubmitRecords(cd, SUBMIT_RULE_OPTIONS_LENIENT),
+ (cd, field) ->
+ parseSubmitRecords(
+ StreamSupport.stream(field.spliterator(), false)
+ .map(f -> new String(f, UTF_8))
+ .collect(toSet()),
+ SUBMIT_RULE_OPTIONS_LENIENT,
+ cd));
public static void parseSubmitRecords(
Collection<String> values, SubmitRuleOptions opts, ChangeData out) {
@@ -987,7 +1056,8 @@
.entries()
.forEach(e -> result.add(e.getValue().toByteArray(e.getKey())));
return result;
- });
+ },
+ (cd, field) -> cd.setRefStates(RefState.parseStates(field)));
/**
* All ref wildcard patterns that were used in the course of indexing this document.
@@ -1013,7 +1083,8 @@
RefStatePattern.create(RefNames.refsDraftCommentsPrefix(id) + "*")
.toByteArray(allUsers(cd)));
return result;
- });
+ },
+ (cd, field) -> cd.setRefStatePatterns(field));
private static String getTopic(ChangeData cd) {
Change c = cd.change();
@@ -1031,6 +1102,18 @@
return Protos.toByteArray(converter.toProto(object));
}
+ private static <T> List<T> decodeProtos(Iterable<byte[]> raw, ProtoConverter<?, T> converter) {
+ return StreamSupport.stream(raw.spliterator(), false)
+ .map(bytes -> parseProtoFrom(bytes, converter))
+ .collect(toImmutableList());
+ }
+
+ private static <P extends MessageLite, T> T parseProtoFrom(
+ byte[] bytes, ProtoConverter<P, T> converter) {
+ P message = Protos.parseUnchecked(converter.getParser(), bytes, 0, bytes.length);
+ return converter.fromProto(message);
+ }
+
private static <T> FieldDef.Getter<ChangeData, T> changeGetter(Function<Change, T> func) {
return in -> in.change() != null ? func.apply(in.change()) : null;
}
diff --git a/java/com/google/gerrit/server/index/change/ChangeIndex.java b/java/com/google/gerrit/server/index/change/ChangeIndex.java
index 49d0d4e..b8a5cd9 100644
--- a/java/com/google/gerrit/server/index/change/ChangeIndex.java
+++ b/java/com/google/gerrit/server/index/change/ChangeIndex.java
@@ -19,7 +19,7 @@
import com.google.gerrit.index.IndexDefinition;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.LegacyChangeIdPredicate;
+import com.google.gerrit.server.query.change.ChangePredicates;
import com.google.gerrit.server.query.change.LegacyChangeIdStrPredicate;
/**
@@ -32,7 +32,7 @@
@Override
default Predicate<ChangeData> keyPredicate(Change.Id id) {
return getSchema().useLegacyNumericFields()
- ? new LegacyChangeIdPredicate(id)
+ ? ChangePredicates.id(id)
: new LegacyChangeIdStrPredicate(id);
}
}
diff --git a/java/com/google/gerrit/server/query/change/AssigneePredicate.java b/java/com/google/gerrit/server/query/change/AssigneePredicate.java
deleted file mode 100644
index 35a91c9..0000000
--- a/java/com/google/gerrit/server/query/change/AssigneePredicate.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// 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.entities.Account;
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class AssigneePredicate extends ChangeIndexPredicate {
- protected final Account.Id id;
-
- public AssigneePredicate(Account.Id id) {
- super(ChangeField.ASSIGNEE, id.toString());
- this.id = id;
- }
-
- @Override
- public boolean match(ChangeData object) {
- if (id.get() == ChangeField.NO_ASSIGNEE) {
- Account.Id assignee = object.change().getAssignee();
- return assignee == null;
- }
- return id.equals(object.change().getAssignee());
- }
-
- @Override
- public int getCost() {
- return 1;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/AttentionSetPredicate.java b/java/com/google/gerrit/server/query/change/AttentionSetPredicate.java
deleted file mode 100644
index 2b18767..0000000
--- a/java/com/google/gerrit/server/query/change/AttentionSetPredicate.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2020 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 com.google.gerrit.server.util.AttentionSetUtil.additionsOnly;
-
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.server.index.change.ChangeField;
-
-/** Simple predicate for searching by attention set. */
-public class AttentionSetPredicate extends ChangeIndexPredicate {
- protected final Account.Id id;
-
- AttentionSetPredicate(Account.Id id) {
- super(ChangeField.ATTENTION_SET_USERS, id.toString());
- this.id = id;
- }
-
- @Override
- public boolean match(ChangeData changeData) {
- return additionsOnly(changeData.attentionSet()).stream()
- .anyMatch(update -> update.account().equals(id));
- }
-
- @Override
- public int getCost() {
- return 1;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/AuthorPredicate.java b/java/com/google/gerrit/server/query/change/AuthorPredicate.java
index 79914a3..6e362ad 100644
--- a/java/com/google/gerrit/server/query/change/AuthorPredicate.java
+++ b/java/com/google/gerrit/server/query/change/AuthorPredicate.java
@@ -28,9 +28,4 @@
public boolean match(ChangeData object) {
return ChangeField.getAuthorParts(object).contains(getValue().toLowerCase());
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/BooleanPredicate.java b/java/com/google/gerrit/server/query/change/BooleanPredicate.java
index 68f83e8..6ca3acc 100644
--- a/java/com/google/gerrit/server/query/change/BooleanPredicate.java
+++ b/java/com/google/gerrit/server/query/change/BooleanPredicate.java
@@ -25,9 +25,4 @@
public boolean match(ChangeData object) {
return getValue().equals(getField().get(object));
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 793e4ec..84c6de0 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -22,6 +22,7 @@
import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
+import com.google.common.collect.HashBasedTable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMap;
@@ -33,6 +34,7 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Table;
import com.google.common.flogger.FluentLogger;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.Nullable;
@@ -319,9 +321,15 @@
private SubmitTypeRecord submitTypeRecord;
private Boolean mergeable;
private Set<String> hashtags;
- private Map<Account.Id, Ref> editsByUser;
+ /** Map from {@link Account.Id} to the tip of the edit ref for this change and a given user. */
+ private Table<Account.Id, PatchSet.Id, ObjectId> editsByUser;
+
private Set<Account.Id> reviewedBy;
- private Map<Account.Id, Ref> draftsByUser;
+ /**
+ * Map from {@link Account.Id} to the tip of the draft comments ref for this change and the user.
+ */
+ private Map<Account.Id, ObjectId> draftsByUser;
+
private ImmutableListMultimap<Account.Id, String> stars;
private StarsOf starsOf;
private ImmutableMap<Account.Id, StarRef> starRefs;
@@ -471,6 +479,26 @@
changedLines = Optional.of(new ChangedLines(insertions, deletions));
}
+ public void setLinesInserted(int insertions) {
+ changedLines =
+ Optional.of(
+ new ChangedLines(
+ insertions,
+ changedLines != null && changedLines.isPresent()
+ ? changedLines.get().deletions
+ : -1));
+ }
+
+ public void setLinesDeleted(int deletions) {
+ changedLines =
+ Optional.of(
+ new ChangedLines(
+ changedLines != null && changedLines.isPresent()
+ ? changedLines.get().insertions
+ : -1,
+ deletions));
+ }
+
public void setNoChangedLines() {
changedLines = Optional.empty();
}
@@ -994,27 +1022,30 @@
}
public Set<Account.Id> editsByUser() {
- return editRefs().keySet();
+ return editRefs().rowKeySet();
}
- public Map<Account.Id, Ref> editRefs() {
+ public Table<Account.Id, PatchSet.Id, ObjectId> editRefs() {
if (editsByUser == null) {
if (!lazyload()) {
- return Collections.emptyMap();
+ return HashBasedTable.create();
}
Change c = change();
if (c == null) {
- return Collections.emptyMap();
+ return HashBasedTable.create();
}
- editsByUser = new HashMap<>();
+ editsByUser = HashBasedTable.create();
Change.Id id = requireNonNull(change.getId());
try (Repository repo = repoManager.openRepository(project())) {
for (Ref ref : repo.getRefDatabase().getRefsByPrefix(RefNames.REFS_USERS)) {
- String name = ref.getName().substring(RefNames.REFS_USERS.length());
- if (id.equals(Change.Id.fromEditRefPart(name))) {
- Account.Id accountId = Account.Id.fromRefPart(name);
+ if (!RefNames.isRefsEdit(ref.getName())) {
+ continue;
+ }
+ PatchSet.Id ps = PatchSet.Id.fromEditRef(ref.getName());
+ if (id.equals(ps.changeId())) {
+ Account.Id accountId = Account.Id.fromRef(ref.getName());
if (accountId != null) {
- editsByUser.put(accountId, ref);
+ editsByUser.put(accountId, ps, ref.getObjectId());
}
}
}
@@ -1029,34 +1060,6 @@
return draftRefs().keySet();
}
- public Map<Account.Id, Ref> draftRefs() {
- if (draftsByUser == null) {
- if (!lazyload()) {
- return Collections.emptyMap();
- }
- Change c = change();
- if (c == null) {
- return Collections.emptyMap();
- }
-
- draftsByUser = new HashMap<>();
- for (Ref ref : commentsUtil.getDraftRefs(notes.getChangeId())) {
- Account.Id account = Account.Id.fromRefSuffix(ref.getName());
- if (account != null
- // Double-check that any drafts exist for this user after
- // filtering out zombies. If some but not all drafts in the ref
- // were zombies, the returned Ref still includes those zombies;
- // this is suboptimal, but is ok for the purposes of
- // draftsByUser(), and easier than trying to rebuild the change at
- // this point.
- && !notes().getDraftComments(account, ref).isEmpty()) {
- draftsByUser.put(account, ref);
- }
- }
- }
- return draftsByUser;
- }
-
public boolean isReviewedBy(Account.Id accountId) {
Collection<String> stars = stars(accountId);
@@ -1211,7 +1214,14 @@
}
ImmutableSetMultimap.Builder<NameKey, RefState> result = ImmutableSetMultimap.builder();
- editRefs().values().forEach(r -> result.put(project, RefState.of(r)));
+ for (Table.Cell<Account.Id, PatchSet.Id, ObjectId> edit : editRefs().cellSet()) {
+ result.put(
+ project,
+ RefState.create(
+ RefNames.refsEdit(
+ edit.getRowKey(), edit.getColumnKey().changeId(), edit.getColumnKey()),
+ edit.getValue()));
+ }
starRefs().values().forEach(r -> result.put(allUsersName, RefState.of(r.ref())));
// TODO: instantiating the notes is too much. We don't want to parse NoteDb, we just want the
@@ -1220,7 +1230,14 @@
notes().getRobotComments(); // Force loading robot comments.
RobotCommentNotes robotNotes = notes().getRobotCommentNotes();
result.put(project, RefState.create(robotNotes.getRefName(), robotNotes.getMetaId()));
- draftRefs().values().forEach(r -> result.put(allUsersName, RefState.of(r)));
+ draftRefs()
+ .entrySet()
+ .forEach(
+ r ->
+ result.put(
+ allUsersName,
+ RefState.create(
+ RefNames.refsDraftComments(getId(), r.getKey()), r.getValue())));
refStates = result.build();
}
@@ -1236,6 +1253,33 @@
public void setRefStates(ImmutableSetMultimap<Project.NameKey, RefState> refStates) {
this.refStates = refStates;
+ if (draftsByUser == null) {
+ // Recover draft refs as well. Draft comments are represented as refs in the repository.
+ // ChangeData exposes #draftsByUser which just provides a Set of Account.Ids of users who
+ // have drafts comments on this change. Recovering this list from RefStates makes it
+ // available even on ChangeData instances retrieved from the index.
+ draftsByUser = new HashMap<>();
+ if (refStates.containsKey(allUsersName)) {
+ refStates.get(allUsersName).stream()
+ .filter(r -> RefNames.isRefsDraftsComments(r.ref()))
+ .forEach(r -> draftsByUser.put(Account.Id.fromRef(r.ref()), r.id()));
+ }
+ }
+ if (editsByUser == null) {
+ // Recover edit refs as well. Edits are represented as refs in the repository.
+ // ChangeData exposes #editsByUser which just provides a Set of Account.Ids of users who
+ // have edits on this change. Recovering this list from RefStates makes it available even
+ // on ChangeData instances retrieved from the index.
+ editsByUser = HashBasedTable.create();
+ if (refStates.containsKey(project())) {
+ refStates.get(project()).stream()
+ .filter(r -> RefNames.isRefsEdit(r.ref()))
+ .forEach(
+ r ->
+ editsByUser.put(
+ Account.Id.fromRef(r.ref()), PatchSet.Id.fromEditRef(r.ref()), r.id()));
+ }
+ }
}
public ImmutableList<byte[]> getRefStatePatterns() {
@@ -1267,4 +1311,32 @@
public abstract ImmutableSortedSet<String> stars();
}
+
+ private Map<Account.Id, ObjectId> draftRefs() {
+ if (draftsByUser == null) {
+ if (!lazyload()) {
+ return Collections.emptyMap();
+ }
+ Change c = change();
+ if (c == null) {
+ return Collections.emptyMap();
+ }
+
+ draftsByUser = new HashMap<>();
+ for (Ref ref : commentsUtil.getDraftRefs(notes.getChangeId())) {
+ Account.Id account = Account.Id.fromRefSuffix(ref.getName());
+ if (account != null
+ // Double-check that any drafts exist for this user after
+ // filtering out zombies. If some but not all drafts in the ref
+ // were zombies, the returned Ref still includes those zombies;
+ // this is suboptimal, but is ok for the purposes of
+ // draftsByUser(), and easier than trying to rebuild the change at
+ // this point.
+ && !notes().getDraftComments(account, ref).isEmpty()) {
+ draftsByUser.put(account, ref.getObjectId());
+ }
+ }
+ }
+ return draftsByUser;
+ }
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
index 05cc6ca..f06d1f2 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
@@ -36,9 +36,4 @@
}
return false;
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java b/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
index a176a58..28bfb0b 100644
--- a/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeIndexPredicate.java
@@ -14,13 +14,16 @@
package com.google.gerrit.server.query.change;
+import com.google.common.primitives.Ints;
import com.google.gerrit.index.FieldDef;
+import com.google.gerrit.index.FieldType;
import com.google.gerrit.index.query.IndexPredicate;
import com.google.gerrit.index.query.Matchable;
import com.google.gerrit.index.query.Predicate;
+import java.util.Objects;
/** Predicate that is mapped to a field in the change index. */
-public abstract class ChangeIndexPredicate extends IndexPredicate<ChangeData>
+public class ChangeIndexPredicate extends IndexPredicate<ChangeData>
implements Matchable<ChangeData> {
/**
* Returns an index predicate that matches no changes in the index.
@@ -41,4 +44,32 @@
protected ChangeIndexPredicate(FieldDef<ChangeData, ?> def, String name, String value) {
super(def, name, value);
}
+
+ @Override
+ public boolean match(ChangeData cd) {
+ if (getField().isRepeatable()) {
+ Iterable<Object> values = (Iterable<Object>) getField().get(cd);
+ for (Object v : values) {
+ if (matchesSingleObject(v)) {
+ return true;
+ }
+ }
+ return false;
+ } else {
+ return matchesSingleObject(getField().get(cd));
+ }
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+
+ private boolean matchesSingleObject(Object fieldValueFromObject) {
+ String fieldTypeName = getField().getType().getName();
+ if (fieldTypeName.equals(FieldType.INTEGER.getName())) {
+ return Objects.equals(fieldValueFromObject, Ints.tryParse(value));
+ }
+ throw new UnsupportedOperationException("match function must be provided in subclass");
+ }
}
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
new file mode 100644
index 0000000..568916d
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -0,0 +1,122 @@
+// Copyright (C) 2021 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.entities.Account;
+import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.PatchSet;
+import com.google.gerrit.index.query.Predicate;
+import com.google.gerrit.server.index.change.ChangeField;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/** Predicates that match against {@link ChangeData}. */
+public class ChangePredicates {
+ private ChangePredicates() {}
+
+ /**
+ * Returns a predicate that matches changes where the provided {@link Account.Id} is in the
+ * attention set.
+ */
+ public static Predicate<ChangeData> attentionSet(Account.Id id) {
+ return new ChangeIndexPredicate(ChangeField.ATTENTION_SET_USERS, id.toString());
+ }
+
+ /**
+ * Returns a predicate that matches changes that are assigned to the provided {@link Account.Id}.
+ */
+ public static Predicate<ChangeData> assignee(Account.Id id) {
+ return new ChangeIndexPredicate(ChangeField.ASSIGNEE, id.toString());
+ }
+
+ /**
+ * Returns a predicate that matches changes that are a revert of the provided {@link Change.Id}.
+ */
+ public static Predicate<ChangeData> revertOf(Change.Id revertOf) {
+ return new ChangeIndexPredicate(ChangeField.REVERT_OF, revertOf.toString());
+ }
+
+ /**
+ * Returns a predicate that matches changes that have a comment authored by the provided {@link
+ * Account.Id}.
+ */
+ public static Predicate<ChangeData> commentBy(Account.Id id) {
+ return new ChangeIndexPredicate(ChangeField.COMMENTBY, id.toString());
+ }
+
+ /**
+ * Returns a predicate that matches changes where the provided {@link Account.Id} has a pending
+ * change edit.
+ */
+ public static Predicate<ChangeData> editBy(Account.Id id) {
+ return new ChangeIndexPredicate(ChangeField.EDITBY, id.toString());
+ }
+
+ /**
+ * Returns a predicate that matches changes where the provided {@link Account.Id} has a pending
+ * draft comment.
+ */
+ public static Predicate<ChangeData> draftBy(Account.Id id) {
+ return new ChangeIndexPredicate(ChangeField.DRAFTBY, id.toString());
+ }
+
+ /**
+ * Returns a predicate that matches changes that were reviewed by any of the provided {@link
+ * Account.Id}.
+ */
+ public static Predicate<ChangeData> reviewedBy(Collection<Account.Id> ids) {
+ List<Predicate<ChangeData>> predicates = new ArrayList<>(ids.size());
+ for (Account.Id id : ids) {
+ predicates.add(new ChangeIndexPredicate(ChangeField.REVIEWEDBY, id.toString()));
+ }
+ return Predicate.or(predicates);
+ }
+
+ /** Returns a predicate that matches changes that were not yet reviewed. */
+ public static Predicate<ChangeData> unreviewed() {
+ return Predicate.not(
+ new ChangeIndexPredicate(ChangeField.REVIEWEDBY, ChangeField.NOT_REVIEWED.toString()));
+ }
+
+ /** Returns a predicate that matches the change with the provided {@link Change.Id}. */
+ public static Predicate<ChangeData> id(Change.Id id) {
+ return new ChangeIndexPredicate(
+ ChangeField.LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
+ }
+
+ /** Returns a predicate that matches changes owned by the provided {@link Account.Id}. */
+ public static Predicate<ChangeData> owner(Account.Id id) {
+ return new ChangeIndexPredicate(ChangeField.OWNER, id.toString());
+ }
+
+ /**
+ * Returns a predicate that matches changes that are a cherry pick of the provided {@link
+ * Change.Id}.
+ */
+ public static Predicate<ChangeData> cherryPickOf(Change.Id id) {
+ return new ChangeIndexPredicate(ChangeField.CHERRY_PICK_OF_CHANGE, id.toString());
+ }
+
+ /**
+ * Returns a predicate that matches changes that are a cherry pick of the provided {@link
+ * PatchSet.Id}.
+ */
+ public static Predicate<ChangeData> cherryPickOf(PatchSet.Id psId) {
+ return Predicate.and(
+ cherryPickOf(psId.changeId()),
+ new ChangeIndexPredicate(ChangeField.CHERRY_PICK_OF_PATCHSET, String.valueOf(psId.get())));
+ }
+}
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 6e2f49c..7ebaec7 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -533,7 +533,7 @@
Integer id = Ints.tryParse(query);
if (id != null) {
return args.getSchema().useLegacyNumericFields()
- ? new LegacyChangeIdPredicate(Change.id(id))
+ ? ChangePredicates.id(Change.id(id))
: new LegacyChangeIdStrPredicate(Change.id(id));
}
} else if (PAT_CHANGE_ID.matcher(query).matches()) {
@@ -551,7 +551,7 @@
@Operator
public Predicate<ChangeData> status(String statusName) {
if ("reviewed".equalsIgnoreCase(statusName)) {
- return IsReviewedPredicate.create();
+ return ChangePredicates.unreviewed();
}
return ChangeStatusPredicate.parse(statusName);
}
@@ -576,7 +576,7 @@
}
if ("edit".equalsIgnoreCase(value)) {
- return new EditByPredicate(self());
+ return ChangePredicates.editBy(self());
}
if ("unresolved".equalsIgnoreCase(value)) {
@@ -610,11 +610,11 @@
}
if ("reviewed".equalsIgnoreCase(value)) {
- return IsReviewedPredicate.create();
+ return ChangePredicates.unreviewed();
}
if ("owner".equalsIgnoreCase(value)) {
- return new OwnerPredicate(self());
+ return ChangePredicates.owner(self());
}
if ("reviewer".equalsIgnoreCase(value)) {
@@ -653,11 +653,11 @@
}
if ("assigned".equalsIgnoreCase(value)) {
- return Predicate.not(new AssigneePredicate(Account.id(ChangeField.NO_ASSIGNEE)));
+ return Predicate.not(ChangePredicates.assignee(Account.id(ChangeField.NO_ASSIGNEE)));
}
if ("unassigned".equalsIgnoreCase(value)) {
- return new AssigneePredicate(Account.id(ChangeField.NO_ASSIGNEE));
+ return ChangePredicates.assignee(Account.id(ChangeField.NO_ASSIGNEE));
}
if ("submittable".equalsIgnoreCase(value)) {
@@ -1060,7 +1060,7 @@
}
private Predicate<ChangeData> draftby(Account.Id who) {
- return new HasDraftByPredicate(who);
+ return ChangePredicates.draftBy(who);
}
@Operator
@@ -1119,9 +1119,9 @@
}
private Predicate<ChangeData> owner(Set<Account.Id> who) {
- List<OwnerPredicate> p = Lists.newArrayListWithCapacity(who.size());
+ List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
for (Account.Id id : who) {
- p.add(new OwnerPredicate(id));
+ p.add(ChangePredicates.owner(id));
}
return Predicate.or(p);
}
@@ -1146,7 +1146,7 @@
}
private Predicate<ChangeData> attention(Set<Account.Id> who) {
- return Predicate.or(who.stream().map(AttentionSetPredicate::new).collect(toImmutableSet()));
+ return Predicate.or(who.stream().map(ChangePredicates::attentionSet).collect(toImmutableSet()));
}
@Operator
@@ -1156,9 +1156,9 @@
}
private Predicate<ChangeData> assignee(Set<Account.Id> who) {
- List<AssigneePredicate> p = Lists.newArrayListWithCapacity(who.size());
+ List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
for (Account.Id id : who) {
- p.add(new AssigneePredicate(id));
+ p.add(ChangePredicates.assignee(id));
}
return Predicate.or(p);
}
@@ -1177,9 +1177,9 @@
}
Set<Account.Id> accounts = getMembers(groupId);
- List<OwnerPredicate> p = Lists.newArrayListWithCapacity(accounts.size());
+ List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(accounts.size());
for (Account.Id id : accounts) {
- p.add(new OwnerPredicate(id));
+ p.add(ChangePredicates.owner(id));
}
return Predicate.or(p);
}
@@ -1275,9 +1275,9 @@
}
private Predicate<ChangeData> commentby(Set<Account.Id> who) {
- List<CommentByPredicate> p = Lists.newArrayListWithCapacity(who.size());
+ List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(who.size());
for (Account.Id id : who) {
- p.add(new CommentByPredicate(id));
+ p.add(ChangePredicates.commentBy(id));
}
return Predicate.or(p);
}
@@ -1337,7 +1337,7 @@
@Operator
public Predicate<ChangeData> reviewedby(String who)
throws QueryParseException, IOException, ConfigInvalidException {
- return IsReviewedPredicate.create(parseAccount(who));
+ return ChangePredicates.reviewedBy(parseAccount(who));
}
@Operator
@@ -1420,8 +1420,11 @@
@Operator
public Predicate<ChangeData> revertof(String value) throws QueryParseException {
+ if (value == null || Ints.tryParse(value) == null) {
+ throw new QueryParseException("'revertof' must be an integer");
+ }
if (args.getSchema().hasField(ChangeField.REVERT_OF)) {
- return new RevertOfPredicate(value);
+ return ChangePredicates.revertOf(Change.id(Ints.tryParse(value)));
}
throw new QueryParseException("'revertof' operator is not supported by change index version");
}
@@ -1440,13 +1443,11 @@
if (args.getSchema().hasField(ChangeField.CHERRY_PICK_OF_CHANGE)
&& args.getSchema().hasField(ChangeField.CHERRY_PICK_OF_PATCHSET)) {
if (Ints.tryParse(value) != null) {
- return new CherryPickOfChangePredicate(value);
+ return ChangePredicates.cherryPickOf(Change.id(Ints.tryParse(value)));
}
try {
PatchSet.Id patchSetId = PatchSet.Id.parse(value);
- return Predicate.and(
- new CherryPickOfChangePredicate(patchSetId.changeId().toString()),
- new CherryPickOfPatchSetPredicate(patchSetId.getId()));
+ return ChangePredicates.cherryPickOf(patchSetId);
} catch (IllegalArgumentException e) {
throw new QueryParseException(
"'"
diff --git a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index 88e93d9..0721433 100644
--- a/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -124,11 +124,6 @@
}
@Override
- public int getCost() {
- return 0;
- }
-
- @Override
public int hashCode() {
return Objects.hashCode(status);
}
diff --git a/java/com/google/gerrit/server/query/change/CherryPickOfChangePredicate.java b/java/com/google/gerrit/server/query/change/CherryPickOfChangePredicate.java
deleted file mode 100644
index d452017..0000000
--- a/java/com/google/gerrit/server/query/change/CherryPickOfChangePredicate.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class CherryPickOfChangePredicate extends ChangeIndexPredicate {
- public CherryPickOfChangePredicate(String cherryPickOfChange) {
- super(ChangeField.CHERRY_PICK_OF_CHANGE, cherryPickOfChange);
- }
-
- @Override
- public boolean match(ChangeData cd) {
- if (cd.change().getCherryPickOf() == null) {
- return false;
- }
- return Integer.toString(cd.change().getCherryPickOf().changeId().get()).equals(value);
- }
-
- @Override
- public int getCost() {
- return 1;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/CherryPickOfPatchSetPredicate.java b/java/com/google/gerrit/server/query/change/CherryPickOfPatchSetPredicate.java
deleted file mode 100644
index 888f45d..0000000
--- a/java/com/google/gerrit/server/query/change/CherryPickOfPatchSetPredicate.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class CherryPickOfPatchSetPredicate extends ChangeIndexPredicate {
- public CherryPickOfPatchSetPredicate(String cherryPickOfPatchSet) {
- super(ChangeField.CHERRY_PICK_OF_PATCHSET, cherryPickOfPatchSet);
- }
-
- @Override
- public boolean match(ChangeData cd) {
- if (cd.change().getCherryPickOf() == null) {
- return false;
- }
- return cd.change().getCherryPickOf().getId().equals(value);
- }
-
- @Override
- public int getCost() {
- return 1;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/CommentByPredicate.java b/java/com/google/gerrit/server/query/change/CommentByPredicate.java
deleted file mode 100644
index b8cf100..0000000
--- a/java/com/google/gerrit/server/query/change/CommentByPredicate.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2015 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.entities.Account;
-import com.google.gerrit.entities.ChangeMessage;
-import com.google.gerrit.entities.HumanComment;
-import com.google.gerrit.server.index.change.ChangeField;
-import java.util.Objects;
-
-public class CommentByPredicate extends ChangeIndexPredicate {
- protected final Account.Id id;
-
- public CommentByPredicate(Account.Id id) {
- super(ChangeField.COMMENTBY, id.toString());
- this.id = id;
- }
-
- Account.Id getAccountId() {
- return id;
- }
-
- @Override
- public boolean match(ChangeData cd) {
- for (ChangeMessage m : cd.messages()) {
- if (Objects.equals(m.getAuthor(), id)) {
- return true;
- }
- }
- for (HumanComment c : cd.publishedComments()) {
- if (Objects.equals(c.author.getId(), id)) {
- return true;
- }
- }
- return false;
- }
-
- @Override
- public int getCost() {
- return 1;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/CommentPredicate.java b/java/com/google/gerrit/server/query/change/CommentPredicate.java
index 4b14f08..0abe45d 100644
--- a/java/com/google/gerrit/server/query/change/CommentPredicate.java
+++ b/java/com/google/gerrit/server/query/change/CommentPredicate.java
@@ -37,7 +37,7 @@
Predicate<ChangeData> p =
Predicate.and(
index.getSchema().useLegacyNumericFields()
- ? new LegacyChangeIdPredicate(id)
+ ? ChangePredicates.id(id)
: new LegacyChangeIdStrPredicate(id),
this);
for (ChangeData cData : index.getSource(p, IndexedChangeQuery.oneResult()).read()) {
@@ -51,9 +51,4 @@
return false;
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/CommitPredicate.java b/java/com/google/gerrit/server/query/change/CommitPredicate.java
index b54ee64..2b3b345 100644
--- a/java/com/google/gerrit/server/query/change/CommitPredicate.java
+++ b/java/com/google/gerrit/server/query/change/CommitPredicate.java
@@ -51,9 +51,4 @@
}
return matchesAbbreviation(p.commitId(), id);
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/CommitterPredicate.java b/java/com/google/gerrit/server/query/change/CommitterPredicate.java
index 1dcf97f..65034a2 100644
--- a/java/com/google/gerrit/server/query/change/CommitterPredicate.java
+++ b/java/com/google/gerrit/server/query/change/CommitterPredicate.java
@@ -28,9 +28,4 @@
public boolean match(ChangeData object) {
return ChangeField.getCommitterParts(object).contains(getValue().toLowerCase());
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
index 16f85b1..80e3cb9 100644
--- a/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ConflictsPredicate.java
@@ -90,7 +90,7 @@
and.add(
Predicate.not(
args.getSchema().useLegacyNumericFields()
- ? new LegacyChangeIdPredicate(c.getId())
+ ? ChangePredicates.id(c.getId())
: new LegacyChangeIdStrPredicate(c.getId())));
and.add(Predicate.or(filePredicates));
diff --git a/java/com/google/gerrit/server/query/change/DirectoryPredicate.java b/java/com/google/gerrit/server/query/change/DirectoryPredicate.java
index 3ab3e26..9249137 100644
--- a/java/com/google/gerrit/server/query/change/DirectoryPredicate.java
+++ b/java/com/google/gerrit/server/query/change/DirectoryPredicate.java
@@ -31,9 +31,4 @@
public boolean match(ChangeData cd) {
return ChangeField.getDirectories(cd).contains(value);
}
-
- @Override
- public int getCost() {
- return 0;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/EditByPredicate.java b/java/com/google/gerrit/server/query/change/EditByPredicate.java
deleted file mode 100644
index 0fd66f8..0000000
--- a/java/com/google/gerrit/server/query/change/EditByPredicate.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2015 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.entities.Account;
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class EditByPredicate extends ChangeIndexPredicate {
- protected final Account.Id id;
-
- public EditByPredicate(Account.Id id) {
- super(ChangeField.EDITBY, id.toString());
- this.id = id;
- }
-
- @Override
- public boolean match(ChangeData cd) {
- return cd.editsByUser().contains(id);
- }
-
- @Override
- public int getCost() {
- return 1;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java b/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
index 9c033b6..4e454fa 100644
--- a/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsFilePredicate.java
@@ -35,9 +35,4 @@
public boolean match(ChangeData object) {
return ChangeField.getFileParts(object).contains(value);
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
index 3e9209b..30d5e2f 100644
--- a/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsLabelPredicate.java
@@ -108,7 +108,15 @@
return false;
}
- if (account != null && !account.equals(approver)) {
+ if (account != null
+ && !account.equals(approver)
+ && !account.equals(ChangeQueryBuilder.OWNER_ACCOUNT_ID)) {
+ return false;
+ }
+
+ if (account != null
+ && account.equals(ChangeQueryBuilder.OWNER_ACCOUNT_ID)
+ && !cd.change().getOwner().equals(approver)) {
return false;
}
diff --git a/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java b/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
index 76936fa..63975d0 100644
--- a/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
+++ b/java/com/google/gerrit/server/query/change/EqualsPathPredicate.java
@@ -26,9 +26,4 @@
public boolean match(ChangeData object) {
return Collections.binarySearch(object.currentFilePaths(), value) >= 0;
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java b/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
index c1b6928..3b3cb90 100644
--- a/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ExactAuthorPredicate.java
@@ -29,9 +29,4 @@
public boolean match(ChangeData object) {
return ChangeField.getAuthorNameAndEmail(object).contains(getValue());
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java b/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
index dac63af..390d4ab 100644
--- a/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ExactCommitterPredicate.java
@@ -29,9 +29,4 @@
public boolean match(ChangeData object) {
return ChangeField.getCommitterNameAndEmail(object).contains(getValue());
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/ExactHashtagPredicate.java b/java/com/google/gerrit/server/query/change/ExactHashtagPredicate.java
index a6526f7..76ced90 100644
--- a/java/com/google/gerrit/server/query/change/ExactHashtagPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ExactHashtagPredicate.java
@@ -37,9 +37,4 @@
}
return false;
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java b/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
index 6683c91..5d8d2c0 100644
--- a/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ExactTopicPredicate.java
@@ -31,9 +31,4 @@
}
return getValue().equals(change.getTopic());
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java b/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java
index bddd2ec..c16bc83 100644
--- a/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java
+++ b/java/com/google/gerrit/server/query/change/FileExtensionListPredicate.java
@@ -36,9 +36,4 @@
public boolean match(ChangeData cd) {
return ChangeField.getAllExtensionsAsList(cd).equals(value);
}
-
- @Override
- public int getCost() {
- return 0;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java b/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
index ee573a7..39715cf 100644
--- a/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
+++ b/java/com/google/gerrit/server/query/change/FileExtensionPredicate.java
@@ -33,9 +33,4 @@
public boolean match(ChangeData object) {
return ChangeField.getExtensions(object).contains(value);
}
-
- @Override
- public int getCost() {
- return 0;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/FooterPredicate.java b/java/com/google/gerrit/server/query/change/FooterPredicate.java
index 4d7588c..37bd6b1 100644
--- a/java/com/google/gerrit/server/query/change/FooterPredicate.java
+++ b/java/com/google/gerrit/server/query/change/FooterPredicate.java
@@ -37,9 +37,4 @@
public boolean match(ChangeData cd) {
return ChangeField.getFooters(cd).contains(value);
}
-
- @Override
- public int getCost() {
- return 0;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/FuzzyHashtagPredicate.java b/java/com/google/gerrit/server/query/change/FuzzyHashtagPredicate.java
index 35c96ef..b4d6b5f 100644
--- a/java/com/google/gerrit/server/query/change/FuzzyHashtagPredicate.java
+++ b/java/com/google/gerrit/server/query/change/FuzzyHashtagPredicate.java
@@ -30,9 +30,4 @@
public boolean match(ChangeData cd) {
return cd.hashtags().stream().anyMatch(ht -> ht.toLowerCase().contains(getValue()));
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java b/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
index d558b0f..47652b8 100644
--- a/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
+++ b/java/com/google/gerrit/server/query/change/FuzzyTopicPredicate.java
@@ -45,7 +45,7 @@
try {
Predicate<ChangeData> thisId =
index.getSchema().useLegacyNumericFields()
- ? new LegacyChangeIdPredicate(cd.getId())
+ ? ChangePredicates.id(cd.getId())
: new LegacyChangeIdStrPredicate(cd.getId());
Iterable<ChangeData> results =
index.getSource(and(thisId, this), IndexedChangeQuery.oneResult()).read();
@@ -54,9 +54,4 @@
throw new StorageException(e);
}
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/GroupPredicate.java b/java/com/google/gerrit/server/query/change/GroupPredicate.java
index 99f37d6..f470cf9 100644
--- a/java/com/google/gerrit/server/query/change/GroupPredicate.java
+++ b/java/com/google/gerrit/server/query/change/GroupPredicate.java
@@ -33,9 +33,4 @@
}
return false;
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java b/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
deleted file mode 100644
index 6d1576f..0000000
--- a/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2010 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.entities.Account;
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class HasDraftByPredicate extends ChangeIndexPredicate {
- protected final Account.Id accountId;
-
- public HasDraftByPredicate(Account.Id accountId) {
- super(ChangeField.DRAFTBY, accountId.toString());
- this.accountId = accountId;
- }
-
- @Override
- public boolean match(ChangeData cd) {
- return cd.draftsByUser().contains(accountId);
- }
-
- @Override
- public int getCost() {
- return 1;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/HasStarsPredicate.java b/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
index 2fbd1e8..6482a19 100644
--- a/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
+++ b/java/com/google/gerrit/server/query/change/HasStarsPredicate.java
@@ -31,11 +31,6 @@
}
@Override
- public int getCost() {
- return 1;
- }
-
- @Override
public String toString() {
return ChangeQueryBuilder.FIELD_STARBY + ":" + accountId;
}
diff --git a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
index e0f7d91..ed0f237 100644
--- a/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
+++ b/java/com/google/gerrit/server/query/change/InternalChangeQuery.java
@@ -99,7 +99,7 @@
predicateFactory =
(id) ->
schema().useLegacyNumericFields()
- ? new LegacyChangeIdPredicate(id)
+ ? ChangePredicates.id(id)
: new LegacyChangeIdStrPredicate(id);
}
diff --git a/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java b/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
deleted file mode 100644
index 4b32b06..0000000
--- a/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (C) 2015 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 com.google.gerrit.server.index.change.ChangeField.REVIEWEDBY;
-
-import com.google.gerrit.entities.Account;
-import com.google.gerrit.index.query.Predicate;
-import com.google.gerrit.server.index.change.ChangeField;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Set;
-
-public class IsReviewedPredicate extends ChangeIndexPredicate {
- protected static final Account.Id NOT_REVIEWED = Account.id(ChangeField.NOT_REVIEWED);
-
- public static Predicate<ChangeData> create() {
- return Predicate.not(new IsReviewedPredicate(NOT_REVIEWED));
- }
-
- public static Predicate<ChangeData> create(Collection<Account.Id> ids) {
- List<Predicate<ChangeData>> predicates = new ArrayList<>(ids.size());
- for (Account.Id id : ids) {
- predicates.add(new IsReviewedPredicate(id));
- }
- return Predicate.or(predicates);
- }
-
- protected final Account.Id id;
-
- private IsReviewedPredicate(Account.Id id) {
- super(REVIEWEDBY, Integer.toString(id.get()));
- this.id = id;
- }
-
- @Override
- public boolean match(ChangeData cd) {
- Set<Account.Id> reviewedBy = cd.reviewedBy();
- return !reviewedBy.isEmpty() ? reviewedBy.contains(id) : id.equals(NOT_REVIEWED);
- }
-
- @Override
- public int getCost() {
- return 1;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
deleted file mode 100644
index d531236..0000000
--- a/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (C) 2010 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 com.google.gerrit.server.index.change.ChangeField.LEGACY_ID;
-
-import com.google.gerrit.entities.Change;
-
-/** Predicate over change number (aka legacy ID or Change.Id). */
-public class LegacyChangeIdPredicate extends ChangeIndexPredicate {
- protected final Change.Id id;
-
- public LegacyChangeIdPredicate(Change.Id id) {
- super(LEGACY_ID, ChangeQueryBuilder.FIELD_CHANGE, id.toString());
- this.id = id;
- }
-
- @Override
- public boolean match(ChangeData object) {
- return id.equals(object.getId());
- }
-
- @Override
- public int getCost() {
- return 1;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/LegacyChangeIdStrPredicate.java b/java/com/google/gerrit/server/query/change/LegacyChangeIdStrPredicate.java
index bae9c0d..60cfd8f 100644
--- a/java/com/google/gerrit/server/query/change/LegacyChangeIdStrPredicate.java
+++ b/java/com/google/gerrit/server/query/change/LegacyChangeIdStrPredicate.java
@@ -31,9 +31,4 @@
public boolean match(ChangeData object) {
return id.equals(object.getId());
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/MessagePredicate.java b/java/com/google/gerrit/server/query/change/MessagePredicate.java
index 44cbd8e..caf751e 100644
--- a/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -36,7 +36,7 @@
Predicate<ChangeData> p =
Predicate.and(
index.getSchema().useLegacyNumericFields()
- ? new LegacyChangeIdPredicate(object.getId())
+ ? ChangePredicates.id(object.getId())
: new LegacyChangeIdStrPredicate(object.getId()),
this);
for (ChangeData cData : index.getSource(p, IndexedChangeQuery.oneResult()).read()) {
@@ -50,9 +50,4 @@
return false;
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/OwnerPredicate.java b/java/com/google/gerrit/server/query/change/OwnerPredicate.java
deleted file mode 100644
index 923a9ca..0000000
--- a/java/com/google/gerrit/server/query/change/OwnerPredicate.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2010 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.entities.Account;
-import com.google.gerrit.entities.Change;
-import com.google.gerrit.server.index.change.ChangeField;
-
-public class OwnerPredicate extends ChangeIndexPredicate {
- protected final Account.Id id;
-
- public OwnerPredicate(Account.Id id) {
- super(ChangeField.OWNER, id.toString());
- this.id = id;
- }
-
- protected Account.Id getAccountId() {
- return id;
- }
-
- @Override
- public boolean match(ChangeData object) {
- Change change = object.change();
- return change != null && id.equals(change.getOwner());
- }
-
- @Override
- public int getCost() {
- return 1;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/ProjectPredicate.java b/java/com/google/gerrit/server/query/change/ProjectPredicate.java
index db5a932..b76207c 100644
--- a/java/com/google/gerrit/server/query/change/ProjectPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ProjectPredicate.java
@@ -37,9 +37,4 @@
Project.NameKey p = change.getDest().project();
return p.equals(getValueKey());
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java b/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
index c23a175..b89fffe 100644
--- a/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ProjectPrefixPredicate.java
@@ -27,9 +27,4 @@
Change c = object.change();
return c != null && c.getDest().project().get().startsWith(getValue());
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/RefPredicate.java b/java/com/google/gerrit/server/query/change/RefPredicate.java
index 1c70a62..8dced69 100644
--- a/java/com/google/gerrit/server/query/change/RefPredicate.java
+++ b/java/com/google/gerrit/server/query/change/RefPredicate.java
@@ -30,9 +30,4 @@
}
return getValue().equals(change.getDest().branch());
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/RevertOfPredicate.java b/java/com/google/gerrit/server/query/change/RevertOfPredicate.java
deleted file mode 100644
index eea1b1e..0000000
--- a/java/com/google/gerrit/server/query/change/RevertOfPredicate.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2017 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.server.index.change.ChangeField;
-
-public class RevertOfPredicate extends ChangeIndexPredicate {
- public RevertOfPredicate(String revertOf) {
- super(ChangeField.REVERT_OF, revertOf);
- }
-
- @Override
- public boolean match(ChangeData cd) {
- if (cd.change().getRevertOf() == null) {
- return false;
- }
- return cd.change().getRevertOf().toString().equals(value);
- }
-
- @Override
- public int getCost() {
- return 1;
- }
-}
diff --git a/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java b/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
index 62fe9e8..3a51972 100644
--- a/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ReviewerByEmailPredicate.java
@@ -45,9 +45,4 @@
public boolean match(ChangeData cd) {
return cd.reviewersByEmail().asTable().get(state, adr) != null;
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
index d783f76..142e956 100644
--- a/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -52,9 +52,4 @@
public boolean match(ChangeData cd) {
return cd.reviewers().asTable().get(state, id) != null;
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/StarPredicate.java b/java/com/google/gerrit/server/query/change/StarPredicate.java
index 788f1a3..4a8fe43 100644
--- a/java/com/google/gerrit/server/query/change/StarPredicate.java
+++ b/java/com/google/gerrit/server/query/change/StarPredicate.java
@@ -34,11 +34,6 @@
}
@Override
- public int getCost() {
- return 1;
- }
-
- @Override
public String toString() {
return ChangeQueryBuilder.FIELD_STAR + ":" + label;
}
diff --git a/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java b/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
index 093447e..b653eb5 100644
--- a/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
+++ b/java/com/google/gerrit/server/query/change/SubmissionIdPredicate.java
@@ -33,9 +33,4 @@
}
return getValue().equals(change.getSubmissionId());
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java b/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
index 4ca684a..ecddbb6 100644
--- a/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
+++ b/java/com/google/gerrit/server/query/change/SubmitRecordPredicate.java
@@ -43,9 +43,4 @@
public boolean match(ChangeData in) {
return ChangeField.formatSubmitRecordValues(in).contains(getValue());
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/SubmittablePredicate.java b/java/com/google/gerrit/server/query/change/SubmittablePredicate.java
index 2018fbc..060a92e 100644
--- a/java/com/google/gerrit/server/query/change/SubmittablePredicate.java
+++ b/java/com/google/gerrit/server/query/change/SubmittablePredicate.java
@@ -30,9 +30,4 @@
return cd.submitRecords(ChangeField.SUBMIT_RULE_OPTIONS_STRICT).stream()
.anyMatch(r -> r.status == status);
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java b/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
index 622fa2c..4f58be2 100644
--- a/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
+++ b/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
@@ -25,9 +25,4 @@
public boolean match(ChangeData cd) {
return cd.trackingFooters().containsValue(getValue());
}
-
- @Override
- public int getCost() {
- return 1;
- }
}
diff --git a/java/com/google/gerrit/server/query/group/GroupPredicates.java b/java/com/google/gerrit/server/query/group/GroupPredicates.java
index 992f60d..9d8171c 100644
--- a/java/com/google/gerrit/server/query/group/GroupPredicates.java
+++ b/java/com/google/gerrit/server/query/group/GroupPredicates.java
@@ -19,6 +19,7 @@
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.index.FieldDef;
import com.google.gerrit.index.query.IndexPredicate;
+import com.google.gerrit.index.query.Matchable;
import com.google.gerrit.index.query.Predicate;
import com.google.gerrit.server.index.group.GroupField;
import java.util.Locale;
@@ -44,7 +45,7 @@
}
public static Predicate<InternalGroup> name(String name) {
- return new GroupPredicate(GroupField.NAME, GroupQueryBuilder.FIELD_NAME, name);
+ return new NameGroupPredicate(name);
}
public static Predicate<InternalGroup> owner(AccountGroup.UUID ownerUuid) {
@@ -75,5 +76,25 @@
}
}
+ // TODO(hiesel): This is just a one-off to make index tests work. Remove in favor of a more
+ // generic solution.
+ // This is required because Gerrit needs to look up groups by name on every request.
+ static class NameGroupPredicate extends IndexPredicate<InternalGroup>
+ implements Matchable<InternalGroup> {
+ NameGroupPredicate(String value) {
+ super(GroupField.NAME, value);
+ }
+
+ @Override
+ public boolean match(InternalGroup group) {
+ return group.getName().equals(getValue());
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+ }
+
private GroupPredicates() {}
}
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
index ec82e1a..d225798 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
@@ -41,8 +41,8 @@
import com.google.gerrit.server.change.ChangeJson;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangePredicates;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.HasDraftByPredicate;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.restapi.change.CommentJson;
import com.google.gerrit.server.restapi.change.CommentJson.HumanCommentFormatter;
@@ -147,7 +147,7 @@
private Predicate<ChangeData> predicate(Account.Id accountId, DeleteDraftCommentsInput input)
throws BadRequestException {
- Predicate<ChangeData> hasDraft = new HasDraftByPredicate(accountId);
+ Predicate<ChangeData> hasDraft = ChangePredicates.draftBy(accountId);
if (CharMatcher.whitespace().trimFrom(Strings.nullToEmpty(input.query)).isEmpty()) {
return hasDraft;
}
diff --git a/java/com/google/gerrit/testing/BUILD b/java/com/google/gerrit/testing/BUILD
index 93c996e8..be32138 100644
--- a/java/com/google/gerrit/testing/BUILD
+++ b/java/com/google/gerrit/testing/BUILD
@@ -12,6 +12,7 @@
"//lib:junit",
"//lib/mockito",
],
+ runtime_deps = ["//java/com/google/gerrit/index/testing"],
deps = [
"//java/com/google/gerrit/acceptance/config",
"//java/com/google/gerrit/acceptance/testsuite/project",
diff --git a/java/com/google/gerrit/testing/InMemoryModule.java b/java/com/google/gerrit/testing/InMemoryModule.java
index e93c921..7ca763a1 100644
--- a/java/com/google/gerrit/testing/InMemoryModule.java
+++ b/java/com/google/gerrit/testing/InMemoryModule.java
@@ -242,6 +242,8 @@
install(luceneIndexModule());
} else if (indexType.isElasticsearch()) {
install(elasticIndexModule());
+ } else if (indexType.isFake()) {
+ install(fakeIndexModule());
}
bind(ServerInformationImpl.class);
bind(ServerInformation.class).to(ServerInformationImpl.class);
@@ -317,6 +319,10 @@
return indexModule("com.google.gerrit.elasticsearch.ElasticIndexModule");
}
+ private Module fakeIndexModule() {
+ return indexModule("com.google.gerrit.index.testing.FakeIndexModule");
+ }
+
private Module indexModule(String moduleClassName) {
try {
Class<?> clazz = Class.forName(moduleClassName);
diff --git a/java/com/google/gerrit/testing/IndexConfig.java b/java/com/google/gerrit/testing/IndexConfig.java
index a8ed5be..daef2b3 100644
--- a/java/com/google/gerrit/testing/IndexConfig.java
+++ b/java/com/google/gerrit/testing/IndexConfig.java
@@ -50,4 +50,8 @@
return cfg;
}
+
+ public static Config createForFake() {
+ return create();
+ }
}
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
index 8cd09e6..4d7468f 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticContainer.java
@@ -39,8 +39,6 @@
private static String getImageName(ElasticVersion version) {
switch (version) {
- case V7_4:
- return "blacktop/elasticsearch:7.4.2";
case V7_5:
return "blacktop/elasticsearch:7.5.2";
case V7_6:
diff --git a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
index 508dc84..75e9636 100644
--- a/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
+++ b/javatests/com/google/gerrit/elasticsearch/ElasticVersionTest.java
@@ -22,9 +22,6 @@
public class ElasticVersionTest {
@Test
public void supportedVersion() throws Exception {
- assertThat(ElasticVersion.forVersion("7.4.0")).isEqualTo(ElasticVersion.V7_4);
- assertThat(ElasticVersion.forVersion("7.4.1")).isEqualTo(ElasticVersion.V7_4);
-
assertThat(ElasticVersion.forVersion("7.5.0")).isEqualTo(ElasticVersion.V7_5);
assertThat(ElasticVersion.forVersion("7.5.1")).isEqualTo(ElasticVersion.V7_5);
diff --git a/javatests/com/google/gerrit/entities/PatchSetTest.java b/javatests/com/google/gerrit/entities/PatchSetTest.java
index 61e1b4c..7e04fe8 100644
--- a/javatests/com/google/gerrit/entities/PatchSetTest.java
+++ b/javatests/com/google/gerrit/entities/PatchSetTest.java
@@ -96,6 +96,10 @@
public void parseId() {
assertThat(PatchSet.Id.parse("1,2")).isEqualTo(PatchSet.id(Change.id(1), 2));
assertThat(PatchSet.Id.parse("01,02")).isEqualTo(PatchSet.id(Change.id(1), 2));
+ Change.Id cId = Change.id(321);
+ assertThat(
+ PatchSet.Id.fromEditRef(RefNames.refsEdit(Account.id(123), cId, PatchSet.id(cId, 6))))
+ .isEqualTo(PatchSet.id(cId, 6));
assertInvalidId(null);
assertInvalidId("");
assertInvalidId("1");
diff --git a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
index b3ef1ea..f6b3317 100644
--- a/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
+++ b/javatests/com/google/gerrit/server/query/account/AbstractQueryAccountsTest.java
@@ -146,6 +146,8 @@
protected abstract Injector createInjector();
+ protected void validateAssumptions() {}
+
@Before
public void setUpInjector() throws Exception {
lifecycle = new LifecycleManager();
@@ -155,6 +157,7 @@
lifecycle.start();
initAfterLifecycleStart();
setUpDatabase();
+ validateAssumptions();
}
@After
diff --git a/javatests/com/google/gerrit/server/query/account/BUILD b/javatests/com/google/gerrit/server/query/account/BUILD
index 5c910a0..4ae2039 100644
--- a/javatests/com/google/gerrit/server/query/account/BUILD
+++ b/javatests/com/google/gerrit/server/query/account/BUILD
@@ -32,8 +32,7 @@
name = "lucene_query_test",
size = "large",
srcs = glob(
- ["*.java"],
- exclude = ABSTRACT_QUERY_TEST,
+ ["LuceneQueryAccountsTest.java"],
),
visibility = ["//visibility:public"],
deps = [
@@ -44,3 +43,21 @@
"//lib/guice",
],
)
+
+junit_tests(
+ name = "fake_query_test",
+ size = "large",
+ srcs = glob(
+ ["FakeQueryAccountsTest.java"],
+ ),
+ visibility = ["//visibility:public"],
+ deps = [
+ ":abstract_query_tests",
+ "//java/com/google/gerrit/entities",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:jgit",
+ "//lib/guice",
+ "@truth//jar",
+ ],
+)
diff --git a/javatests/com/google/gerrit/server/query/account/FakeQueryAccountsTest.java b/javatests/com/google/gerrit/server/query/account/FakeQueryAccountsTest.java
new file mode 100644
index 0000000..b742bd8
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/account/FakeQueryAccountsTest.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2021 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.account;
+
+import static org.junit.Assume.assumeFalse;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.index.account.AccountSchemaDefinitions;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
+import com.google.gerrit.testing.IndexVersions;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import java.sql.Timestamp;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.lib.Config;
+
+public class FakeQueryAccountsTest extends AbstractQueryAccountsTest {
+ @ConfigSuite.Default
+ public static Config defaultConfig() {
+ return IndexConfig.createForFake();
+ }
+
+ @ConfigSuite.Configs
+ public static Map<String, Config> againstPreviousIndexVersion() {
+ // the current schema version is already tested by the inherited default config suite
+ List<Integer> schemaVersions =
+ IndexVersions.getWithoutLatest(AccountSchemaDefinitions.INSTANCE);
+ return IndexVersions.asConfigMap(
+ AccountSchemaDefinitions.INSTANCE, schemaVersions, "againstIndexVersion", defaultConfig());
+ }
+
+ @Override
+ protected Injector createInjector() {
+ Config fakeConfig = new Config(config);
+ InMemoryModule.setDefaults(fakeConfig);
+ fakeConfig.setString("index", null, "type", "fake");
+ return Guice.createInjector(new InMemoryModule(fakeConfig));
+ }
+
+ @Override
+ protected void validateAssumptions() {
+ // TODO(hiesel): Account predicates are always matching (they return true on match), so we need
+ // to skip all tests here. We are doing this to document existing behavior. We want to remove
+ // this assume statement and make group predicates matchable.
+ assumeFalse(
+ AccountPredicates.equalsName("test")
+ .asMatchable()
+ .match(
+ AccountState.forAccount(Account.builder(Account.id(1), new Timestamp(0)).build())));
+ }
+}
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 4e84b3c..6c8026b 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -1905,7 +1905,7 @@
assertQuery("-added:<=4");
assertQuery("added:3", change1);
- assertQuery("-(added:<3 OR added>3)", change1);
+ assertQuery("-(added:<3 OR added:>3)", change1);
assertQuery("added:>2", change1);
assertQuery("-added:<=2", change1);
@@ -1923,7 +1923,7 @@
assertQuery("-deleted:<=3");
assertQuery("deleted:2", change2);
- assertQuery("-(deleted:<2 OR deleted>2)", change2);
+ assertQuery("-(deleted:<2 OR deleted:>2)", change2);
assertQuery("deleted:>1", change2);
assertQuery("-deleted:<=1", change2);
@@ -2039,7 +2039,7 @@
assertQuery("user@example.com", expected);
assertQuery("repo", expected);
- assertQuery("Code-Review:+1", change4);
+ assertQuery("Code-Review=+1", change4);
}
@Test
diff --git a/javatests/com/google/gerrit/server/query/change/BUILD b/javatests/com/google/gerrit/server/query/change/BUILD
index 43b9690..0f102a8 100644
--- a/javatests/com/google/gerrit/server/query/change/BUILD
+++ b/javatests/com/google/gerrit/server/query/change/BUILD
@@ -4,6 +4,7 @@
ABSTRACT_QUERY_TEST = [
"AbstractQueryChangesTest.java",
"LuceneQueryChangesTest.java",
+ "FakeQueryChangesTest.java",
]
java_library(
@@ -24,6 +25,7 @@
"//java/com/google/gerrit/httpd",
"//java/com/google/gerrit/index",
"//java/com/google/gerrit/index:query_exception",
+ "//java/com/google/gerrit/index/testing",
"//java/com/google/gerrit/lifecycle",
"//java/com/google/gerrit/server",
"//java/com/google/gerrit/server/project/testing:project-test-util",
@@ -38,9 +40,11 @@
],
)
-LUCENE_QUERY_TEST = [
+QUERY_TEST = [
"LuceneQueryChangesLatestIndexVersionTest.java",
"LuceneQueryChangesPreviousIndexVersionTest.java",
+ "FakeQueryChangesLatestIndexVersionTest.java",
+ "FakeQueryChangesPreviousIndexVersionTest.java",
]
[junit_tests(
@@ -60,14 +64,14 @@
"//lib/guice",
"//lib/truth",
],
-) for f in LUCENE_QUERY_TEST]
+) for f in QUERY_TEST]
junit_tests(
name = "small_tests",
size = "small",
srcs = glob(
["*.java"],
- exclude = ABSTRACT_QUERY_TEST + LUCENE_QUERY_TEST,
+ exclude = ABSTRACT_QUERY_TEST + QUERY_TEST,
),
visibility = ["//visibility:public"],
deps = [
diff --git a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesLatestIndexVersionTest.java b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesLatestIndexVersionTest.java
new file mode 100644
index 0000000..5496f56
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesLatestIndexVersionTest.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2021 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.testing.ConfigSuite;
+import com.google.gerrit.testing.IndexConfig;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Test against {@link com.google.gerrit.index.testing.AbstractFakeIndex} using the latest schema.
+ */
+public class FakeQueryChangesLatestIndexVersionTest extends FakeQueryChangesTest {
+ @ConfigSuite.Default
+ public static Config defaultConfig() {
+ return IndexConfig.createForFake();
+ }
+}
diff --git a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesPreviousIndexVersionTest.java b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesPreviousIndexVersionTest.java
new file mode 100644
index 0000000..1610eca
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesPreviousIndexVersionTest.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2021 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.common.collect.Iterables;
+import com.google.gerrit.server.index.change.ChangeSchemaDefinitions;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.IndexConfig;
+import com.google.gerrit.testing.IndexVersions;
+import org.eclipse.jgit.lib.Config;
+
+/**
+ * Test against {@link com.google.gerrit.index.testing.AbstractFakeIndex} using the current schema.
+ */
+public class FakeQueryChangesPreviousIndexVersionTest extends FakeQueryChangesTest {
+ @ConfigSuite.Default
+ public static Config againstPreviousIndexVersion() {
+ // the current schema version is already tested by the inherited default config suite
+ return Iterables.getOnlyElement(
+ IndexVersions.asConfigMap(
+ ChangeSchemaDefinitions.INSTANCE,
+ IndexVersions.getWithoutLatest(ChangeSchemaDefinitions.INSTANCE),
+ "againstIndexVersion",
+ IndexConfig.createForFake())
+ .values());
+ }
+}
diff --git a/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
new file mode 100644
index 0000000..385d4b2
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/change/FakeQueryChangesTest.java
@@ -0,0 +1,189 @@
+// Copyright (C) 2021 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.testing.InMemoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import org.eclipse.jgit.lib.Config;
+import org.junit.Ignore;
+import org.junit.Test;
+
+/**
+ * Test against {@link com.google.gerrit.index.testing.AbstractFakeIndex}. This test might seem
+ * obsolete, but it makes sure that the fake index implementation used in tests gives the same
+ * results as production indices.
+ */
+public abstract class FakeQueryChangesTest extends AbstractQueryChangesTest {
+ @Override
+ protected Injector createInjector() {
+ Config fakeConfig = new Config(config);
+ InMemoryModule.setDefaults(fakeConfig);
+ fakeConfig.setString("index", null, "type", "fake");
+ return Guice.createInjector(new InMemoryModule(fakeConfig));
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void byDefault() throws Exception {
+ // TODO(hiesel): Fix bug in predicate and remove @Ignore.
+ super.byDefault();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void byMergedBefore() throws Exception {
+ // TODO(hiesel): Used predicate is not a matchable. Fix.
+ super.byMergedBefore();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void reviewerAndCcByEmail() throws Exception {
+ // TODO(hiesel): Fix bug in predicate and remove @Ignore.
+ super.reviewerAndCcByEmail();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void byMessageExact() throws Exception {
+ // TODO(hiesel): Existing #match function uses the index causing a StackOverflowError. Fix.
+ super.byMessageExact();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void fullTextWithNumbers() throws Exception {
+ // TODO(hiesel): Existing #match function uses the index causing a StackOverflowError. Fix.
+ super.fullTextWithNumbers();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void byTriplet() throws Exception {
+ // TODO(hiesel): Fix bug in predicate and remove @Ignore.
+ super.byTriplet();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void byAge() throws Exception {
+ // TODO(hiesel): Existing #match function uses the index causing a StackOverflowError. Fix.
+ super.byAge();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void byMessageSubstring() throws Exception {
+ // TODO(hiesel): Existing #match function uses the index causing a StackOverflowError. Fix.
+ super.byMessageSubstring();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void byBeforeUntil() throws Exception {
+ // TODO(hiesel): Used predicate is not a matchable. Fix.
+ super.byBeforeUntil();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void byTopic() throws Exception {
+ // TODO(hiesel): Existing #match function uses the index causing a StackOverflowError. Fix.
+ super.byTopic();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void userQuery() throws Exception {
+ // TODO(hiesel): Account name predicate is always returning true in #match. Fix.
+ super.userQuery();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void visible() throws Exception {
+ // TODO(hiesel): Account name predicate is always returning true in #match. Fix.
+ super.visible();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void userDestination() throws Exception {
+ // TODO(hiesel): Account name predicate is always returning true in #match. Fix.
+ super.userDestination();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void byAfterSince() throws Exception {
+ // TODO(hiesel): Used predicate is not a matchable. Fix.
+ super.byAfterSince();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void byMessageMixedCase() throws Exception {
+ // TODO(hiesel): Used predicate is not a matchable. Fix.
+ super.byMessageMixedCase();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void byCommit() throws Exception {
+ // TODO(hiesel): Existing #match function uses the index causing a StackOverflowError. Fix.
+ super.byCommit();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void byComment() throws Exception {
+ // TODO(hiesel): Existing #match function uses the index causing a StackOverflowError. Fix.
+ super.byComment();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void byMergedAfter() throws Exception {
+ // TODO(hiesel): Used predicate is not a matchable. Fix.
+ super.byMergedAfter();
+ }
+
+ @Ignore
+ @Test
+ @Override
+ public void byOwnerInvalidQuery() throws Exception {
+ // TODO(hiesel): Account name predicate is always returning true in #match. Fix.
+ super.byMergedAfter();
+ }
+}
diff --git a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
index a822417..f392747 100644
--- a/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
+++ b/javatests/com/google/gerrit/server/query/group/AbstractQueryGroupsTest.java
@@ -115,6 +115,8 @@
protected abstract Injector createInjector();
+ protected void validateAssumptions() {}
+
@Before
public void setUpInjector() throws Exception {
lifecycle = new LifecycleManager();
@@ -124,6 +126,7 @@
lifecycle.start();
initAfterLifecycleStart();
setUpDatabase();
+ validateAssumptions();
}
@After
diff --git a/javatests/com/google/gerrit/server/query/group/BUILD b/javatests/com/google/gerrit/server/query/group/BUILD
index e14350f..4b74325 100644
--- a/javatests/com/google/gerrit/server/query/group/BUILD
+++ b/javatests/com/google/gerrit/server/query/group/BUILD
@@ -29,8 +29,7 @@
name = "lucene_query_test",
size = "large",
srcs = glob(
- ["*.java"],
- exclude = ABSTRACT_QUERY_TEST,
+ ["LuceneQueryGroupsTest.java"],
),
visibility = ["//visibility:public"],
deps = [
@@ -41,3 +40,20 @@
"//lib/guice",
],
)
+
+junit_tests(
+ name = "fake_query_test",
+ size = "large",
+ srcs = glob(
+ ["FakeQueryGroupsTest.java"],
+ ),
+ visibility = ["//visibility:public"],
+ deps = [
+ ":abstract_query_tests",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:jgit",
+ "//lib/guice",
+ "@truth//jar",
+ ],
+)
diff --git a/javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java b/javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java
new file mode 100644
index 0000000..8bc1c30
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/group/FakeQueryGroupsTest.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2021 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.group;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.google.gerrit.server.index.group.GroupSchemaDefinitions;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
+import com.google.gerrit.testing.IndexVersions;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.lib.Config;
+
+public class FakeQueryGroupsTest extends AbstractQueryGroupsTest {
+ @ConfigSuite.Default
+ public static Config defaultConfig() {
+ return IndexConfig.createForFake();
+ }
+
+ @ConfigSuite.Configs
+ public static Map<String, Config> againstPreviousIndexVersion() {
+ // the current schema version is already tested by the inherited default config suite
+ List<Integer> schemaVersions = IndexVersions.getWithoutLatest(GroupSchemaDefinitions.INSTANCE);
+ return IndexVersions.asConfigMap(
+ GroupSchemaDefinitions.INSTANCE, schemaVersions, "againstIndexVersion", defaultConfig());
+ }
+
+ @Override
+ protected Injector createInjector() {
+ Config fakeConfig = new Config(config);
+ InMemoryModule.setDefaults(fakeConfig);
+ fakeConfig.setString("index", null, "type", "fake");
+ return Guice.createInjector(new InMemoryModule(fakeConfig));
+ }
+
+ @Override
+ protected void validateAssumptions() {
+ // TODO(hiesel): Group predicates are not matchable, so we need to skip all tests here.
+ // We are doing this to document existing behavior. We want to remove this assume statement and
+ // make group predicates matchable.
+ assumeTrue(GroupPredicates.inname("test").isMatchable());
+ }
+}
diff --git a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
index dfd7928..2317c7e 100644
--- a/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
+++ b/javatests/com/google/gerrit/server/query/project/AbstractQueryProjectsTest.java
@@ -111,6 +111,8 @@
protected abstract Injector createInjector();
+ protected void validateAssumptions() {}
+
@Before
public void setUpInjector() throws Exception {
lifecycle = new LifecycleManager();
@@ -120,6 +122,7 @@
lifecycle.start();
initAfterLifecycleStart();
setUpDatabase();
+ validateAssumptions();
}
@After
diff --git a/javatests/com/google/gerrit/server/query/project/BUILD b/javatests/com/google/gerrit/server/query/project/BUILD
index 984d824..a65306c 100644
--- a/javatests/com/google/gerrit/server/query/project/BUILD
+++ b/javatests/com/google/gerrit/server/query/project/BUILD
@@ -29,8 +29,7 @@
name = "lucene_query_test",
size = "large",
srcs = glob(
- ["*.java"],
- exclude = ABSTRACT_QUERY_TEST,
+ ["LuceneQueryProjectsTest.java"],
),
visibility = ["//visibility:public"],
deps = [
@@ -41,3 +40,21 @@
"//lib/guice",
],
)
+
+junit_tests(
+ name = "fake_query_test",
+ size = "large",
+ srcs = glob(
+ ["FakeQueryProjectsTest.java"],
+ ),
+ visibility = ["//visibility:public"],
+ deps = [
+ ":abstract_query_tests",
+ "//java/com/google/gerrit/index/project",
+ "//java/com/google/gerrit/server",
+ "//java/com/google/gerrit/testing:gerrit-test-util",
+ "//lib:jgit",
+ "//lib/guice",
+ "@truth//jar",
+ ],
+)
diff --git a/javatests/com/google/gerrit/server/query/project/FakeQueryProjectsTest.java b/javatests/com/google/gerrit/server/query/project/FakeQueryProjectsTest.java
new file mode 100644
index 0000000..8517ad2
--- /dev/null
+++ b/javatests/com/google/gerrit/server/query/project/FakeQueryProjectsTest.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2021 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.project;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.google.gerrit.index.project.ProjectSchemaDefinitions;
+import com.google.gerrit.testing.ConfigSuite;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.gerrit.testing.IndexConfig;
+import com.google.gerrit.testing.IndexVersions;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jgit.lib.Config;
+
+public class FakeQueryProjectsTest extends AbstractQueryProjectsTest {
+ @ConfigSuite.Default
+ public static Config defaultConfig() {
+ return IndexConfig.createForFake();
+ }
+
+ @ConfigSuite.Configs
+ public static Map<String, Config> againstPreviousIndexVersion() {
+ // the current schema version is already tested by the inherited default config suite
+ List<Integer> schemaVersions =
+ IndexVersions.getWithoutLatest(
+ com.google.gerrit.index.project.ProjectSchemaDefinitions.INSTANCE);
+ return IndexVersions.asConfigMap(
+ ProjectSchemaDefinitions.INSTANCE, schemaVersions, "againstIndexVersion", defaultConfig());
+ }
+
+ @Override
+ protected Injector createInjector() {
+ Config fakeConfig = new Config(config);
+ InMemoryModule.setDefaults(fakeConfig);
+ fakeConfig.setString("index", null, "type", "fake");
+ return Guice.createInjector(new InMemoryModule(fakeConfig));
+ }
+
+ @Override
+ protected void validateAssumptions() {
+ // TODO(hiesel): Project predicates are not matchable, so we need to skip all tests here.
+ // We are doing this to document existing behavior. We want to remove this assume statement and
+ // make group predicates matchable.
+ assumeTrue(ProjectPredicates.inname("test").isMatchable());
+ }
+}
diff --git a/polygerrit-ui/app/api/checks.ts b/polygerrit-ui/app/api/checks.ts
index e74eee1..454b3d5 100644
--- a/polygerrit-ui/app/api/checks.ts
+++ b/polygerrit-ui/app/api/checks.ts
@@ -225,8 +225,6 @@
name: string;
tooltip?: string;
/**
- * TODO: Maybe drop this property? Do we really need it?
- *
* Primary actions will get a more prominent treatment in the UI. For example
* primary actions might be rendered as buttons versus just menu entries in
* an overflow menu.
@@ -424,8 +422,6 @@
url: string;
tooltip?: string;
/**
- * TODO: Maybe drop this property? Do we really need it?
- *
* Primary links will get a more prominent treatment in the UI, e.g. being
* always visible in the results table or also showing up in the change page
* summary of checks.
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-action.ts b/polygerrit-ui/app/elements/checks/gr-checks-action.ts
index 33e52a7..82680aa 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-action.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-action.ts
@@ -59,7 +59,10 @@
@click="${(e: Event) => this.handleClick(e)}"
>
${this.action.name}
- <paper-tooltip ?hidden="${!this.action.tooltip}" offset="5"
+ <paper-tooltip
+ ?hidden="${!this.action.tooltip}"
+ offset="5"
+ fit-to-visible-bounds="true"
>${this.action.tooltip}</paper-tooltip
>
</gr-button>
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index b77912d..4b7aec5 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -28,7 +28,6 @@
} from 'lit-element';
import {GrLitElement} from '../lit/gr-lit-element';
import './gr-checks-action';
-import './gr-checks-attempt';
import '@polymer/paper-tooltip/paper-tooltip';
import {
Action,
@@ -53,6 +52,8 @@
hasCompletedWithoutResults,
iconForCategory,
iconForLink,
+ otherLinks,
+ primaryLink,
primaryRunAction,
tooltipForLink,
} from '../../services/checks/checks-util';
@@ -299,14 +300,13 @@
<div class="flex">
<gr-hovercard-run .run="${this.result}"></gr-hovercard-run>
<div class="name">${this.result.checkName}</div>
- <gr-checks-attempt .run="${this.result}"></gr-checks-attempt>
<div class="space"></div>
${this.renderPrimaryRunAction()}
</div>
</td>
<td class="summaryCol">
<div class="summary-cell">
- ${(this.result.links?.slice(0, 1) ?? []).map(this.renderLink)}
+ ${this.renderLink(primaryLink(this.result))}
${this.renderSummary(this.result.summary)}
<div class="message" @click="${this.toggleExpanded}">
${this.isExpanded ? '' : this.result.message}
@@ -386,6 +386,7 @@
if (category !== Category.ERROR && category !== Category.WARNING) return;
const label = this.result?.labelName;
if (!label) return;
+ if (!this.result?.isLatestAttempt) return;
const info = this.labels?.[label];
const status = getLabelStatus(info).toLowerCase();
const value = valueString(getRepresentativeValue(info));
@@ -396,12 +397,13 @@
}
renderLinks() {
- const links = (this.result?.links ?? []).slice(1);
+ const links = otherLinks(this.result);
if (links.length === 0) return;
return html`<div class="links">${links.map(this.renderLink)}</div>`;
}
- renderLink(link: Link) {
+ renderLink(link?: Link) {
+ if (!link) return;
const tooltipText = link.tooltip ?? tooltipForLink(link.icon);
return html`<a href="${link.url}" target="_blank"
><iron-icon
@@ -667,8 +669,15 @@
display: flex;
align-items: center;
}
- .headerBottomRow .links iron-icon {
+ .headerBottomRow iron-icon {
color: var(--link-color);
+ }
+ .headerBottomRow .space {
+ display: inline-block;
+ width: var(--spacing-xl);
+ height: var(--line-height-normal);
+ }
+ .headerBottomRow a {
margin-right: var(--spacing-l);
}
#moreActions iron-icon {
@@ -831,7 +840,11 @@
</div>
<div class="headerBottomRow">
<div class="left">${this.renderFilter()}</div>
- <div class="right">${this.renderLinks()}${this.renderActions()}</div>
+ <div class="right">
+ ${this.renderLinks()}
+ <div class="space"></div>
+ ${this.renderActions()}
+ </div>
</div>
</div>
<div class="body">
@@ -844,12 +857,43 @@
}
private renderLinks() {
- const links = (this.links ?? []).slice(0, 4);
+ const links = this.links ?? [];
if (links.length === 0) return;
- return html`<div class="links">${links.map(this.renderLink)}</div>`;
+ const primaryLinks = links.filter(a => a.primary).slice(0, 4);
+ const overflowLinks = links.filter(a => !primaryLinks.includes(a));
+ return html`
+ ${primaryLinks.map(this.renderLink)}
+ ${this.renderOverflowLinks(overflowLinks)}
+ `;
}
- private renderLink(link: Link) {
+ private renderOverflowLinks(overflowLinks: Link[]) {
+ const items = overflowLinks.map(link => {
+ return {
+ ...link,
+ id: link.tooltip,
+ name: link.tooltip,
+ target: '_blank',
+ tooltip: undefined,
+ };
+ });
+ return html`
+ <gr-dropdown
+ id="moreLinks"
+ link=""
+ vertical-offset="32"
+ horizontal-align="right"
+ .items="${items}"
+ >
+ <iron-icon icon="gr-icons:more-vert" aria-labelledby="moreMessage">
+ </iron-icon>
+ <span id="moreMessage">More</span>
+ </gr-dropdown>
+ `;
+ }
+
+ private renderLink(link?: Link) {
+ if (!link) return;
const tooltipText = link.tooltip ?? tooltipForLink(link.icon);
return html`<a href="${link.url}" target="_blank"
><iron-icon
@@ -862,23 +906,33 @@
}
private renderActions() {
- const overflowItems = this.actions.slice(2).map(action => {
+ const actions = this.actions ?? [];
+ if (actions.length === 0) return;
+ const primaryActions = actions.filter(a => a.primary).slice(0, 2);
+ const overflowActions = actions.filter(a => !primaryActions.includes(a));
+ return html`
+ ${this.renderAction(primaryActions[0])}
+ ${this.renderAction(primaryActions[1])}
+ ${this.renderOverflowActions(overflowActions)}
+ `;
+ }
+
+ private renderOverflowActions(overflowActions: Action[]) {
+ const items = overflowActions.map(action => {
return {...action, id: action.name};
});
- const disabledItems = overflowItems
+ if (!items || items.length === 0) return;
+ const disabledItems = items
.filter(action => action.disabled)
.map(action => action.id);
return html`
- ${this.renderAction(this.actions[0])}
- ${this.renderAction(this.actions[1])}
<gr-dropdown
id="moreActions"
link=""
vertical-offset="32"
horizontal-align="right"
@tap-item="${this.handleAction}"
- ?hidden="${overflowItems.length === 0}"
- .items="${overflowItems}"
+ .items="${items}"
.disabledIds="${disabledItems}"
>
<iron-icon icon="gr-icons:more-vert" aria-labelledby="moreMessage">
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
index 175a0b5..edcc856 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-runs.ts
@@ -61,6 +61,8 @@
} from './gr-checks-util';
import {ChecksTabState} from '../../types/events';
import {charsOnly} from '../../utils/string-util';
+import {appContext} from '../../services/app-context';
+import {KnownExperimentId} from '../../services/flags/flags';
@customElement('gr-checks-run')
export class GrChecksRun extends GrLitElement {
@@ -327,6 +329,8 @@
private isSectionExpanded = new Map<RunStatus, boolean>();
+ private flagService = appContext.flagsService;
+
constructor() {
super();
this.subscribe('runs', allRuns$);
@@ -425,34 +429,7 @@
/>
${this.renderSection(RunStatus.COMPLETED)}
${this.renderSection(RunStatus.RUNNING)}
- ${this.renderSection(RunStatus.RUNNABLE)}
- <div class="testing">
- <div>Toggle fake runs by clicking buttons:</div>
- <gr-button link @click="${this.none}">none</gr-button>
- <gr-button
- link
- @click="${() =>
- this.toggle('f0', [fakeRun0], fakeActions, fakeLinks)}"
- >0</gr-button
- >
- <gr-button link @click="${() => this.toggle('f1', [fakeRun1])}"
- >1</gr-button
- >
- <gr-button link @click="${() => this.toggle('f2', [fakeRun2])}"
- >2</gr-button
- >
- <gr-button link @click="${() => this.toggle('f3', [fakeRun3])}"
- >3</gr-button
- >
- <gr-button
- link
- @click="${() => {
- this.toggle('f4', [fakeRun4_1, fakeRun4_2, fakeRun4_3, fakeRun4_4]);
- }}"
- >4</gr-button
- >
- <gr-button link @click="${this.all}">all</gr-button>
- </div>
+ ${this.renderSection(RunStatus.RUNNABLE)} ${this.renderFakeControls()}
`;
}
@@ -541,6 +518,39 @@
}
return show;
}
+
+ renderFakeControls() {
+ if (!this.flagService.isEnabled(KnownExperimentId.CHECKS_DEVELOPER)) return;
+ return html`
+ <div class="testing">
+ <div>Toggle fake runs by clicking buttons:</div>
+ <gr-button link @click="${this.none}">none</gr-button>
+ <gr-button
+ link
+ @click="${() =>
+ this.toggle('f0', [fakeRun0], fakeActions, fakeLinks)}"
+ >0</gr-button
+ >
+ <gr-button link @click="${() => this.toggle('f1', [fakeRun1])}"
+ >1</gr-button
+ >
+ <gr-button link @click="${() => this.toggle('f2', [fakeRun2])}"
+ >2</gr-button
+ >
+ <gr-button link @click="${() => this.toggle('f3', [fakeRun3])}"
+ >3</gr-button
+ >
+ <gr-button
+ link
+ @click="${() => {
+ this.toggle('f4', [fakeRun4_1, fakeRun4_2, fakeRun4_3, fakeRun4_4]);
+ }}"
+ >4</gr-button
+ >
+ <gr-button link @click="${this.all}">all</gr-button>
+ </div>
+ `;
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
index 9b7c25a..69c6139 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor.ts
@@ -35,7 +35,6 @@
import {PolymerDomWrapper} from '../../../types/types';
import {GrDiffGroupType} from '../gr-diff/gr-diff-group';
import {GrDiff} from '../gr-diff/gr-diff';
-import {fireAlert, fireEvent} from '../../../utils/event-util';
import {Subscription} from 'rxjs';
import {toggleClass} from '../../../utils/dom-util';
@@ -44,9 +43,6 @@
const LEFT_SIDE_CLASS = 'target-side-left';
const RIGHT_SIDE_CLASS = 'target-side-right';
-// Time in which pressing n key again after the toast navigates to next file
-const NAVIGATE_TO_NEXT_FILE_TIMEOUT_MS = 5000;
-
export interface GrDiffCursor {
$: {};
}
@@ -59,8 +55,6 @@
private preventAutoScrollOnManualScroll = false;
- private lastDisplayedNavigateToFileToast: Map<string, number> = new Map();
-
@property({type: String})
side = Side.RIGHT;
@@ -190,66 +184,28 @@
}
}
- private showToastAndFireEvent(direction: string, shortcut: string) {
- /*
- * If user presses p/n on the first/last diff chunk, show a toast informing
- * user that pressing it again will navigate them to previous/next
- * unreviewedfile if click happens within the time limit
- */
- if (
- this.lastDisplayedNavigateToFileToast.get(direction) &&
- Date.now() - this.lastDisplayedNavigateToFileToast.get(direction)! <=
- NAVIGATE_TO_NEXT_FILE_TIMEOUT_MS
- ) {
- // reset for next file
- this.lastDisplayedNavigateToFileToast.delete(direction);
- fireEvent(this, `navigate-to-${direction}-unreviewed-file`);
- } else {
- this.lastDisplayedNavigateToFileToast.set(direction, Date.now());
- fireAlert(
- this,
- `Press ${shortcut} again to navigate to ${direction} unreviewed file`
- );
- }
- }
-
- moveToNextChunk(
- clipToTop?: boolean,
- navigateToNextFile?: boolean
- ): CursorMoveResult {
+ moveToNextChunk(clipToTop?: boolean): CursorMoveResult {
const result = this.cursorManager.next({
filter: (row: HTMLElement) => this._isFirstRowOfChunk(row),
getTargetHeight: target =>
(target?.parentNode as HTMLElement)?.scrollHeight || 0,
clipToTop,
});
- if (
- navigateToNextFile &&
- result === CursorMoveResult.CLIPPED &&
- this.isAtEnd()
- ) {
- this.showToastAndFireEvent('next', 'n');
- }
-
this._fixSide();
return result;
}
- moveToPreviousChunk(navigateToPreviousFile?: boolean): CursorMoveResult {
+ moveToPreviousChunk(): CursorMoveResult {
const result = this.cursorManager.previous({
filter: (row: HTMLElement) => this._isFirstRowOfChunk(row),
});
- if (navigateToPreviousFile && this.isAtStart()) {
- this.showToastAndFireEvent('previous', 'p');
- }
this._fixSide();
return result;
}
- moveToNextCommentThread(): CursorMoveResult | undefined {
+ moveToNextCommentThread(): CursorMoveResult {
if (this.isAtEnd()) {
- fireEvent(this, 'navigate-to-next-file-with-comments');
- return;
+ return CursorMoveResult.CLIPPED;
}
const result = this.cursorManager.next({
filter: (row: HTMLElement) => this._rowHasThread(row),
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
index 901f72a..7b72da8 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-cursor/gr-diff-cursor_test.js
@@ -445,21 +445,6 @@
});
});
- test('navigate to next unreviewed file via moveToNextChunk', () => {
- const cursorManager = cursorElement.cursorManager;
- cursorManager.index = cursorManager.stops.length - 1;
- const dispatchEventStub = sinon.stub(cursorElement, 'dispatchEvent');
- cursorElement.moveToNextChunk(/* opt_clipToTop = */false,
- /* opt_navigateToNextFile = */true);
- assert.isTrue(dispatchEventStub.called);
- assert.equal(dispatchEventStub.getCall(1).args[0].type, 'show-alert');
-
- cursorElement.moveToNextChunk(/* opt_clipToTop = */false,
- /* opt_navigateToNextFile = */true);
- assert.equal(dispatchEventStub.getCall(2).args[0].type,
- 'navigate-to-next-unreviewed-file');
- });
-
test('initialLineNumber not provided', done => {
let scrollBehaviorDuringMove;
const moveToNumStub = sinon.stub(cursorElement, 'moveToLineNumber');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts
index ca003f3..6216644 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-highlight/gr-diff-highlight.ts
@@ -123,7 +123,10 @@
* because isMouseUp === true combined with an existing selection must
* mean that this is the end of a double-click.
*/
- handleSelectionChange(selection: Selection | null, isMouseUp: boolean) {
+ handleSelectionChange(
+ selection: Selection | Range | null,
+ isMouseUp: boolean
+ ) {
if (selection === null) return;
// Debounce is not just nice for waiting until the selection has settled,
// it is also vital for being able to click on the action box before it is
@@ -262,7 +265,7 @@
* syntax highligh, convert native DOM Range objects to Gerrit concepts
* (line, side, etc).
*/
- _getNormalizedRange(selection: Selection) {
+ _getNormalizedRange(selection: Selection | Range) {
/* On Safari the ShadowRoot.getSelection() isn't there and the only thing
we can get is a single Range */
if (selection instanceof Range) {
@@ -425,7 +428,7 @@
);
}
- _handleSelection(selection: Selection, isMouseUp: boolean) {
+ _handleSelection(selection: Selection | Range, isMouseUp: boolean) {
/* On Safari, the selection events may return a null range that should
be ignored */
if (!selection) {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index 638b49e..b7ac0de 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -106,10 +106,15 @@
import {GerritView} from '../../../services/router/router-model';
import {assertIsDefined} from '../../../utils/common-util';
import {toggleClass, getKeyboardEvent} from '../../../utils/dom-util';
+import {CursorMoveResult} from '../../shared/gr-cursor-manager/gr-cursor-manager';
+
const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
const MSG_LOADING_BLAME = 'Loading blame...';
const MSG_LOADED_BLAME = 'Blame loaded';
+// Time in which pressing n key again after the toast navigates to next file
+const NAVIGATE_TO_NEXT_FILE_TIMEOUT_MS = 5000;
+
interface Files {
sortedFileList: string[];
changeFilesByPath: {[path: string]: FileInfo};
@@ -623,17 +628,62 @@
e.preventDefault();
if (e.detail.keyboardEvent?.shiftKey) {
- this.$.cursor.moveToNextCommentThread();
+ const result = this.$.cursor.moveToNextCommentThread();
+ if (result === CursorMoveResult.CLIPPED) {
+ this._navigateToNextFileWithCommentThread();
+ }
} else {
if (this.modifierPressed(e)) return;
+ const result = this.$.cursor.moveToNextChunk();
// navigate to next file if key is not being held down
- this.$.cursor.moveToNextChunk(
- /* opt_clipToTop = */ false,
- /* opt_navigateToNextFile = */ !e.detail.keyboardEvent?.repeat
+ if (
+ !e.detail.keyboardEvent?.repeat &&
+ result === CursorMoveResult.CLIPPED &&
+ this.$.cursor.isAtEnd()
+ ) {
+ this.showToastAndNavigateFile('next', 'n');
+ }
+ }
+ }
+
+ private lastDisplayedNavigateToFileToast: Map<string, number> = new Map();
+
+ private showToastAndNavigateFile(direction: string, shortcut: string) {
+ /*
+ * If user presses p/n on the first/last diff chunk, show a toast informing
+ * user that pressing it again will navigate them to previous/next
+ * unreviewedfile if click happens within the time limit
+ */
+ if (
+ this.lastDisplayedNavigateToFileToast.get(direction) &&
+ Date.now() - this.lastDisplayedNavigateToFileToast.get(direction)! <=
+ NAVIGATE_TO_NEXT_FILE_TIMEOUT_MS
+ ) {
+ // reset for next file
+ this.lastDisplayedNavigateToFileToast.delete(direction);
+ this.navigateToUnreviewedFile(direction);
+ } else {
+ this.lastDisplayedNavigateToFileToast.set(direction, Date.now());
+ fireAlert(
+ this,
+ `Press ${shortcut} again to navigate to ${direction} unreviewed file`
);
}
}
+ private navigateToUnreviewedFile(direction: string) {
+ if (!this._path) return;
+ if (!this._fileList) return;
+ if (!this._reviewedFiles) return;
+ // Ensure that the currently viewed file always appears in unreviewedFiles
+ // so we resolve the right "next" file.
+ const unreviewedFiles = this._fileList.filter(
+ file => file === this._path || !this._reviewedFiles.has(file)
+ );
+
+ this._navToFile(this._path, unreviewedFiles, direction === 'next' ? 1 : -1);
+ }
+
_handlePrevChunkOrCommentThread(e: CustomKeyboardEvent) {
if (this.shouldSuppressKeyboardShortcut(e)) return;
@@ -642,7 +692,10 @@
this.$.cursor.moveToPreviousCommentThread();
} else {
if (this.modifierPressed(e)) return;
- this.$.cursor.moveToPreviousChunk(!e.detail.keyboardEvent?.repeat);
+ this.$.cursor.moveToPreviousChunk();
+ if (!e.detail.keyboardEvent?.repeat && this.$.cursor.isAtStart()) {
+ this.showToastAndNavigateFile('previous', 'p');
+ }
}
}
@@ -1753,34 +1806,10 @@
return disableDiffPrefs || !loggedIn;
}
- _navigateToNextUnreviewedFile() {
- if (!this._path) return;
- if (!this._fileList) return;
- if (!this._reviewedFiles) return;
- // Ensure that the currently viewed file always appears in unreviewedFiles
- // so we resolve the right "next" file.
- const unreviewedFiles = this._fileList.filter(
- file => file === this._path || !this._reviewedFiles.has(file)
- );
- this._navToFile(this._path, unreviewedFiles, 1);
- }
-
- _navigateToPreviousUnreviewedFile() {
- if (!this._path) return;
- if (!this._fileList) return;
- if (!this._reviewedFiles) return;
- // Ensure that the currently viewed file always appears in unreviewedFiles
- // so we resolve the right "next" file.
- const unreviewedFiles = this._fileList.filter(
- file => file === this._path || !this._reviewedFiles.has(file)
- );
- this._navToFile(this._path, unreviewedFiles, -1);
- }
-
_handleNextUnreviewedFile(e: CustomKeyboardEvent) {
if (this.shouldSuppressKeyboardShortcut(e)) return;
this._setReviewed(true);
- this._navigateToNextUnreviewedFile();
+ this.navigateToUnreviewedFile('next');
}
_navigateToNextFileWithCommentThread() {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
index 63bf74e..7897a4a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_html.ts
@@ -429,11 +429,6 @@
on-reload-diff-preference="_handleReloadingDiffPreference"
>
</gr-diff-preferences-dialog>
- <gr-diff-cursor
- id="cursor"
- on-navigate-to-next-unreviewed-file="_navigateToNextUnreviewedFile"
- on-navigate-to-previous-unreviewed-file="_navigateToPreviousUnreviewedFile"
- on-navigate-to-next-file-with-comments="_navigateToNextFileWithCommentThread"
- ></gr-diff-cursor>
+ <gr-diff-cursor id="cursor"></gr-diff-cursor>
<gr-comment-api id="commentAPI"></gr-comment-api>
`;
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
index 244bafb..39ec384 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view_test.js
@@ -29,6 +29,8 @@
createComment,
} from '../../../test/test-data-generators.js';
import {EditPatchSetNum} from '../../../types/common.js';
+import sinon from 'sinon/pkg/sinon-esm';
+import {CursorMoveResult} from '../../shared/gr-cursor-manager/gr-cursor-manager.js';
const basicFixture = fixtureFromElement('gr-diff-view');
@@ -1735,6 +1737,133 @@
});
});
+ suite('switching files', () => {
+ let dispatchEventStub;
+ let navToFileStub;
+ let moveToPreviousChunkStub;
+ let moveToNextChunkStub;
+ let isAtStartStub;
+ let isAtEndStub;
+ let nowStub;
+
+ setup(() => {
+ dispatchEventStub = sinon.stub(
+ element, 'dispatchEvent').callThrough();
+ navToFileStub = sinon.stub(element, '_navToFile');
+ moveToPreviousChunkStub =
+ sinon.stub(element.$.cursor, 'moveToPreviousChunk');
+ moveToNextChunkStub =
+ sinon.stub(element.$.cursor, 'moveToNextChunk');
+ isAtStartStub = sinon.stub(element.$.cursor, 'isAtStart');
+ isAtEndStub = sinon.stub(element.$.cursor, 'isAtEnd');
+ nowStub = sinon.stub(Date, 'now');
+ });
+
+ test('shows toast when at the end of file', () => {
+ moveToNextChunkStub.returns(CursorMoveResult.CLIPPED);
+ isAtEndStub.returns(true);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+
+ assert.isTrue(moveToNextChunkStub.called);
+ assert.equal(dispatchEventStub.lastCall.args[0].type, 'show-alert');
+ assert.isFalse(navToFileStub.called);
+ });
+
+ test('navigates to next file when n is tapped again', () => {
+ moveToNextChunkStub.returns(CursorMoveResult.CLIPPED);
+ isAtEndStub.returns(true);
+
+ element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
+ element._reviewedFiles = new Set(['file2']);
+ element._path = 'file1';
+
+ nowStub.returns(5);
+ MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+ nowStub.returns(10);
+ MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+
+ assert.isTrue(navToFileStub.called);
+ assert.deepEqual(navToFileStub.lastCall.args, [
+ 'file1',
+ ['file1', 'file3'],
+ 1,
+ ]);
+ });
+
+ test('does not navigate if n is tapped twice too slow', () => {
+ moveToNextChunkStub.returns(CursorMoveResult.CLIPPED);
+ isAtEndStub.returns(true);
+
+ nowStub.returns(5);
+ MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+ nowStub.returns(6000);
+ MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+
+ assert.isFalse(navToFileStub.called);
+ });
+
+ test('shows toast when at the start of file', () => {
+ moveToPreviousChunkStub.returns(CursorMoveResult.CLIPPED);
+ isAtStartStub.returns(true);
+
+ MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
+
+ assert.isTrue(moveToPreviousChunkStub.called);
+ assert.equal(dispatchEventStub.lastCall.args[0].type, 'show-alert');
+ assert.isFalse(navToFileStub.called);
+ });
+
+ test('navigates to prev file when p is tapped again', () => {
+ moveToPreviousChunkStub.returns(CursorMoveResult.CLIPPED);
+ isAtStartStub.returns(true);
+
+ element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
+ element._reviewedFiles = new Set(['file2']);
+ element._path = 'file3';
+
+ nowStub.returns(5);
+ MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
+ nowStub.returns(10);
+ MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
+
+ assert.isTrue(navToFileStub.called);
+ assert.deepEqual(navToFileStub.lastCall.args, [
+ 'file3',
+ ['file1', 'file3'],
+ -1,
+ ]);
+ });
+
+ test('does not navigate if p is tapped twice too slow', () => {
+ moveToPreviousChunkStub.returns(CursorMoveResult.CLIPPED);
+ isAtStartStub.returns(true);
+
+ nowStub.returns(5);
+ MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
+ nowStub.returns(6000);
+ MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
+
+ assert.isFalse(navToFileStub.called);
+ });
+
+ test('does not navigate when tapping n then p', () => {
+ moveToNextChunkStub.returns(CursorMoveResult.CLIPPED);
+ isAtEndStub.returns(true);
+
+ nowStub.returns(5);
+ MockInteractions.pressAndReleaseKeyOn(element, 78, null, 'n');
+
+ moveToPreviousChunkStub.returns(CursorMoveResult.CLIPPED);
+ isAtStartStub.returns(true);
+
+ nowStub.returns(10);
+ MockInteractions.pressAndReleaseKeyOn(element, 80, null, 'p');
+
+ assert.isFalse(navToFileStub.called);
+ });
+ });
+
test('shift+m navigates to next unreviewed file', () => {
element._files = getFilesFromFileList(['file1', 'file2', 'file3']);
element._reviewedFiles = new Set(['file1', 'file2']);
@@ -1894,7 +2023,7 @@
});
});
- suite('gr-diff-view tests unmodified files with comments', () => {
+ suite('unmodified files with comments', () => {
let element;
setup(() => {
const changedFiles = {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
index 3b76698..486bf3a 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.ts
@@ -65,9 +65,7 @@
import {AbortStop} from '../../shared/gr-cursor-manager/gr-cursor-manager';
import {fireAlert, fireEvent} from '../../../utils/event-util';
import {MovedLinkClickedEvent} from '../../../types/events';
-// TODO(davido): See: https://github.com/GoogleChromeLabs/shadow-selection-polyfill/issues/9
-// @ts-ignore
-import * as shadow from 'shadow-selection-polyfill/shadow.js';
+import {getContentEditableRange} from '../../../utils/safari-selection-util';
import {
CreateCommentEventDetail as CreateCommentEventDetailApi,
@@ -228,6 +226,14 @@
revisionImage?: ImageInfo;
/**
+ * In order to allow multi-select in Safari browsers, a workaround is required
+ * to trigger 'beforeinput' events to get a list of static ranges. This is
+ * obtained by making the content of the diff table "contentEditable".
+ */
+ @property({type: Boolean})
+ isContentEditable = isSafari();
+
+ /**
* Whether the safety check for large diffs when whole-file is set has
* been bypassed. If the value is null, then the safety has not been
* bypassed. If the value is a number, then that number represents the
@@ -336,14 +342,11 @@
@observe('loggedIn', 'isAttached')
_enableSelectionObserver(loggedIn: boolean, isAttached: boolean) {
if (loggedIn && isAttached) {
- document.addEventListener(
- '-shadow-selectionchange',
- this.handleSelectionChange
- );
+ document.addEventListener('selectionchange', this.handleSelectionChange);
document.addEventListener('mouseup', this.handleMouseUp);
} else {
document.removeEventListener(
- '-shadow-selectionchange',
+ 'selectionchange',
this.handleSelectionChange
);
document.removeEventListener('mouseup', this.handleMouseUp);
@@ -375,7 +378,7 @@
return this.root instanceof ShadowRoot && this.root.getSelection
? this.root.getSelection()
: isSafari()
- ? shadow.getRange(this.root)
+ ? getContentEditableRange()
: document.getSelection();
}
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
index e2bc312..9da3bf1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff_html.ts
@@ -97,6 +97,10 @@
height: 100%;
background-color: var(--diff-blank-background-color);
}
+ td.lineNum {
+ vertical-align: top;
+ }
+
/*
The only way to focus this (clicking) will apply our own focus styling,
so this default styling is not needed and distracting.
@@ -358,6 +362,9 @@
color: var(--link-color);
padding: var(--spacing-m) 0 var(--spacing-m) 48px;
}
+ #diffTable:focus {
+ outline: none;
+ }
#loadingError,
#sizeWarning {
display: none;
@@ -558,6 +565,7 @@
id="diffTable"
class$="[[_diffTableClass]]"
role="presentation"
+ contenteditable$="[[isContentEditable]]"
></table>
<template
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
index 6125d33..b3d1578 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.ts
@@ -122,7 +122,7 @@
_currentSearchString?: string;
@property({type: Boolean})
- _hideAutocomplete = true;
+ _hideEmojiAutocomplete = true;
@property({type: Number})
_index?: number;
@@ -141,7 +141,7 @@
get keyBindings() {
return {
esc: '_handleEscKey',
- tab: '_handleEnterByKey',
+ tab: '_handleTabKey',
enter: '_handleEnterByKey',
up: '_handleUpKey',
down: '_handleDownKey',
@@ -186,7 +186,7 @@
}
_handleEscKey(e: KeyboardEvent) {
- if (this._hideAutocomplete) {
+ if (this._hideEmojiAutocomplete) {
return;
}
e.preventDefault();
@@ -195,7 +195,7 @@
}
_handleUpKey(e: KeyboardEvent) {
- if (this._hideAutocomplete) {
+ if (this._hideEmojiAutocomplete) {
return;
}
e.preventDefault();
@@ -206,7 +206,7 @@
}
_handleDownKey(e: KeyboardEvent) {
- if (this._hideAutocomplete) {
+ if (this._hideEmojiAutocomplete) {
return;
}
e.preventDefault();
@@ -216,8 +216,10 @@
this.disableEnterKeyForSelectingEmoji = false;
}
- _handleEnterByKey(e: KeyboardEvent) {
- if (this._hideAutocomplete || this.disableEnterKeyForSelectingEmoji) {
+ _handleTabKey(e: KeyboardEvent) {
+ // Tab should have normal behavior if the picker is closed or if the user
+ // has only typed ':'.
+ if (this._hideEmojiAutocomplete || this.disableEnterKeyForSelectingEmoji) {
return;
}
e.preventDefault();
@@ -225,6 +227,19 @@
this._setEmoji(this.$.emojiSuggestions.getCurrentText());
}
+ _handleEnterByKey(e: KeyboardEvent) {
+ // Enter should have newline behavior if the picker is closed or if the user
+ // has only typed ':'.
+ if (this._hideEmojiAutocomplete || this.disableEnterKeyForSelectingEmoji) {
+ this.indent(e);
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+ this._setEmoji(this.$.emojiSuggestions.getCurrentText());
+ }
+
_handleEmojiSelect(e: CustomEvent) {
this._setEmoji(e.detail.selected.dataset['value']);
}
@@ -257,7 +272,7 @@
* this allows the dropdown to appear near where the user is typing.
*/
_updateCaratPosition() {
- this._hideAutocomplete = false;
+ this._hideEmojiAutocomplete = false;
if (typeof this.$.textarea.value === 'string') {
this.$.hiddenText.textContent = this.$.textarea.value.substr(
0,
@@ -375,7 +390,7 @@
// hide and reset the autocomplete dropdown.
flush();
this._currentSearchString = '';
- this._hideAutocomplete = true;
+ this._hideEmojiAutocomplete = true;
this.closeDropdown();
this._colonIndex = null;
this.$.textarea.textarea.focus();
@@ -386,6 +401,34 @@
new CustomEvent('value-changed', {detail: {value: text}})
);
}
+
+ private indent(e: KeyboardEvent): void {
+ if (!document.queryCommandSupported('insertText')) {
+ return;
+ }
+ // When nothing is selected, selectionStart is the caret position. We want
+ // the indentation level of the current line, not the end of the text which
+ // may be different.
+ const currentLine = this.$.textarea.textarea.value
+ .substr(0, this.$.textarea.selectionStart)
+ .split('\n')
+ .pop();
+ const currentLineIndentation = currentLine?.match(/^\s*/)?.[0];
+ if (!currentLineIndentation) {
+ return;
+ }
+
+ // Stops the normal newline being added afterwards since we are adding it
+ // ourselves.
+ e.preventDefault();
+
+ // MDN says that execCommand is deprecated, but the replacements are still
+ // WIP (Input Events Level 2). The queryCommandSupported check should ensure
+ // that entering newlines will work even if this indent feature breaks.
+ // Directly replacing the text is possible, but would destroy the undo/redo
+ // queue.
+ document.execCommand('insertText', false, '\n' + currentLineIndentation);
+ }
}
declare global {
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.js
index 5b3a2b2..1747fce 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.js
@@ -71,7 +71,7 @@
flush();
assert.isFalse(element.$.emojiSuggestions.isHidden);
assert.equal(element._colonIndex, 0);
- assert.isFalse(element._hideAutocomplete);
+ assert.isFalse(element._hideEmojiAutocomplete);
assert.equal(element._currentSearchString, '');
});
@@ -86,7 +86,7 @@
flush();
assert.isFalse(element.$.emojiSuggestions.isHidden);
assert.equal(element._colonIndex, 1);
- assert.isFalse(element._hideAutocomplete);
+ assert.isFalse(element._hideEmojiAutocomplete);
assert.equal(element._currentSearchString, '');
});
@@ -100,7 +100,7 @@
element.text = 'test:';
flush();
assert.isTrue(element.$.emojiSuggestions.isHidden);
- assert.isTrue(element._hideAutocomplete);
+ assert.isTrue(element._hideEmojiAutocomplete);
});
test('emoji selector opens when a colon is typed and some substring',
@@ -117,7 +117,7 @@
flush();
assert.isFalse(element.$.emojiSuggestions.isHidden);
assert.equal(element._colonIndex, 0);
- assert.isFalse(element._hideAutocomplete);
+ assert.isFalse(element._hideEmojiAutocomplete);
assert.equal(element._currentSearchString, 't');
});
@@ -142,7 +142,7 @@
flush();
assert.isFalse(element.$.emojiSuggestions.isHidden);
assert.equal(element._colonIndex, 0);
- assert.isFalse(element._hideAutocomplete);
+ assert.isFalse(element._hideEmojiAutocomplete);
assert.equal(element._currentSearchString, '');
});
test('emoji selector closes when text changes before the colon', () => {
@@ -169,7 +169,7 @@
const closeSpy = sinon.spy(element, 'closeDropdown');
element._resetEmojiDropdown();
assert.equal(element._currentSearchString, '');
- assert.isTrue(element._hideAutocomplete);
+ assert.isTrue(element._hideEmojiAutocomplete);
assert.equal(element._colonIndex, null);
element.$.emojiSuggestions.open();
@@ -220,6 +220,16 @@
element.$.caratSpan.outerHTML);
});
+ test('newline receives matching indentation', async () => {
+ const indentCommand = sinon.stub(document, 'execCommand');
+ element.$.textarea.value = ' a';
+ element._handleEnterByKey(
+ new CustomEvent('keydown', {detail: {keyboardEvent: {keyCode: 13}}})
+ );
+ await flush();
+ assert.deepEqual(indentCommand.args[0], ['insertText', false, '\n ']);
+ });
+
test('emoji dropdown is closed when iron-overlay-closed is fired', () => {
const resetSpy = sinon.spy(element, '_resetEmojiDropdown');
element.$.emojiSuggestions.dispatchEvent(
diff --git a/polygerrit-ui/app/package.json b/polygerrit-ui/app/package.json
index 6d5e475..4f5acf5 100644
--- a/polygerrit-ui/app/package.json
+++ b/polygerrit-ui/app/package.json
@@ -40,8 +40,7 @@
"page": "^1.11.6",
"polymer-bridges": "file:../../polymer-bridges/",
"polymer-resin": "^2.0.1",
- "rxjs": "^6.6.7",
- "shadow-selection-polyfill": "^1.1.0"
+ "rxjs": "^6.6.7"
},
"license": "Apache-2.0",
"private": true
diff --git a/polygerrit-ui/app/services/checks/checks-model.ts b/polygerrit-ui/app/services/checks/checks-model.ts
index e92db53..60cb780 100644
--- a/polygerrit-ui/app/services/checks/checks-model.ts
+++ b/polygerrit-ui/app/services/checks/checks-model.ts
@@ -228,9 +228,9 @@
privateState$.next(nextState);
}
-// TODO(brohlfs): Remove all fake runs by end of April. They are just making
-// it easier to develop the UI and always see all the different types/states of
-// runs and results.
+// TODO(brohlfs): Remove all fake runs once the Checks UI is fully launched.
+// They are just making it easier to develop the UI and always see all the
+// different types/states of runs and results.
export const fakeRun0: CheckRun = {
internalRunId: 'f0',
@@ -262,7 +262,7 @@
},
{
name: 'Flag',
- tooltip: 'Flag this result as not useful',
+ tooltip: 'Flag this result as totally absolutely really not useful',
primary: true,
disabled: true,
callback: () => Promise.resolve({message: 'flag "flag" triggered'}),
@@ -448,7 +448,7 @@
export const fakeActions: Action[] = [
{
name: 'Fake Action 1',
- primary: false,
+ primary: true,
disabled: true,
tooltip: 'Tooltip for Fake Action 1',
callback: () => Promise.resolve({message: 'fake action 1 triggered'}),
@@ -471,7 +471,7 @@
export const fakeLinks: Link[] = [
{
url: 'https://www.google.com',
- primary: false,
+ primary: true,
tooltip: 'Tooltip for Bug Report Fake Link',
icon: LinkIcon.REPORT_BUG,
},
diff --git a/polygerrit-ui/app/services/checks/checks-util.ts b/polygerrit-ui/app/services/checks/checks-util.ts
index 7eb2a5d..981a1f6 100644
--- a/polygerrit-ui/app/services/checks/checks-util.ts
+++ b/polygerrit-ui/app/services/checks/checks-util.ts
@@ -21,6 +21,7 @@
CheckResult as CheckResultApi,
LinkIcon,
RunStatus,
+ Link,
} from '../../api/checks';
import {assertNever} from '../../utils/common-util';
import {CheckResult, CheckRun} from './checks-model';
@@ -308,3 +309,15 @@
internalResultId: 'fake',
};
}
+
+export function primaryLink(result?: CheckResultApi): Link | undefined {
+ const links = result?.links ?? [];
+ return links.find(link => link.primary);
+}
+
+export function otherLinks(result?: CheckResultApi): Link[] {
+ const primary = primaryLink(result);
+ const links = result?.links ?? [];
+ // Just filter the one primary link, not all primary links.
+ return links.filter(link => link !== primary);
+}
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 1a29055..789ff4e 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -26,4 +26,5 @@
export enum KnownExperimentId {
NEW_IMAGE_DIFF_UI = 'UiFeature__new_image_diff_ui',
TOKEN_HIGHLIGHTING = 'UiFeature__token_highlighting',
+ CHECKS_DEVELOPER = 'UiFeature__checks_developer',
}
diff --git a/polygerrit-ui/app/utils/label-util.ts b/polygerrit-ui/app/utils/label-util.ts
index 0cfbbbd..bcc92e8 100644
--- a/polygerrit-ui/app/utils/label-util.ts
+++ b/polygerrit-ui/app/utils/label-util.ts
@@ -66,10 +66,12 @@
export function getLabelStatus(label?: DetailedLabelInfo): LabelStatus {
const value = getRepresentativeValue(label);
const range = getVotingRangeOrDefault(label);
- if (value === range.min) return LabelStatus.REJECTED;
- if (value === range.max) return LabelStatus.APPROVED;
- if (value < 0) return LabelStatus.DISLIKED;
- if (value > 0) return LabelStatus.RECOMMENDED;
+ if (value < 0) {
+ return value === range.min ? LabelStatus.REJECTED : LabelStatus.DISLIKED;
+ }
+ if (value > 0) {
+ return value === range.max ? LabelStatus.APPROVED : LabelStatus.RECOMMENDED;
+ }
return LabelStatus.NEUTRAL;
}
diff --git a/polygerrit-ui/app/utils/label-util_test.ts b/polygerrit-ui/app/utils/label-util_test.ts
index aef776d..56941ba 100644
--- a/polygerrit-ui/app/utils/label-util_test.ts
+++ b/polygerrit-ui/app/utils/label-util_test.ts
@@ -33,6 +33,10 @@
DetailedLabelInfo,
} from '../types/common';
+const VALUES_0 = {
+ '0': 'neutral',
+};
+
const VALUES_1 = {
'-1': 'bad',
'0': 'neutral',
@@ -134,6 +138,10 @@
test('getLabelStatus', () => {
let labelInfo: DetailedLabelInfo = {all: [], values: VALUES_2};
assert.equal(getLabelStatus(labelInfo), LabelStatus.NEUTRAL);
+ labelInfo = {all: [{value: 0}], values: VALUES_0};
+ assert.equal(getLabelStatus(labelInfo), LabelStatus.NEUTRAL);
+ labelInfo = {all: [{value: 0}], values: VALUES_1};
+ assert.equal(getLabelStatus(labelInfo), LabelStatus.NEUTRAL);
labelInfo = {all: [{value: 0}], values: VALUES_2};
assert.equal(getLabelStatus(labelInfo), LabelStatus.NEUTRAL);
labelInfo = {all: [{value: 1}], values: VALUES_2};
diff --git a/polygerrit-ui/app/utils/safari-selection-util.ts b/polygerrit-ui/app/utils/safari-selection-util.ts
new file mode 100644
index 0000000..e38111f
--- /dev/null
+++ b/polygerrit-ui/app/utils/safari-selection-util.ts
@@ -0,0 +1,114 @@
+/**
+ * @license
+ * Copyright (C) 2021 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.
+ */
+import {isSafari, findActiveElement} from './dom-util';
+
+const SUPPORTS_SHADOW_SELECTION =
+ typeof window.ShadowRoot.prototype.getSelection === 'function';
+const SUPPORTS_BEFORE_INPUT =
+ typeof (window.InputEvent.prototype as InputEventExtended).getTargetRanges ===
+ 'function';
+
+const TARGET_ID = 'diffTable';
+
+let processing = false;
+let contentEditableRange: Range | null = null;
+
+interface InputEventExtended extends InputEvent {
+ getTargetRanges(): StaticRange[];
+}
+
+if (isSafari() && !SUPPORTS_SHADOW_SELECTION && SUPPORTS_BEFORE_INPUT) {
+ /**
+ * This library aims at extracting the selection range in a content editable
+ * area. It is a hacky solution to work around the fact that Safari does not
+ * allow to get the selection from shadow dom anymore.
+ *
+ * The main idea behind this approach is the following:
+ * - Listen to 'selectionChange' events of 'contentEditable' areas.
+ * - Trigger a 'beforeInput' event by running and immediately terminating
+ * an arbitrary `execCommand`.
+ * - use the getTargetRanges() method to get a list of static ranges
+ *
+ * This typescript snippet is the porting of that idea (as explained by its
+ * original author [1]).
+ *
+ * [1] https://github.com/GoogleChromeLabs/shadow-selection-polyfill/issues/11
+ */
+
+ window.addEventListener(
+ 'selectionchange',
+ () => {
+ if (!processing) {
+ processing = true;
+ const active = findActiveElement(document, true);
+ if (active && active.id === TARGET_ID) {
+ // Safari does not allow to select inside a shadowRoot, so we use an
+ // `execCommand` to trigger a `beforeInput` event in order to
+ // get at the target range from the event.
+ document.execCommand('indent');
+ }
+ processing = false;
+ }
+ },
+ true
+ );
+
+ window.addEventListener(
+ 'beforeinput',
+ event => {
+ if (processing) {
+ // selecting
+ const inputEvent = event as InputEventExtended;
+ if (typeof inputEvent.getTargetRanges !== 'function') return;
+ const range = inputEvent.getTargetRanges()[0];
+
+ const newRange = new Range();
+
+ newRange.setStart(range.startContainer, range.startOffset);
+ newRange.setEnd(range.endContainer, range.endOffset);
+
+ contentEditableRange = newRange;
+
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ } else {
+ // typing
+ const active = findActiveElement(document, true);
+ if (active && active.id === TARGET_ID) {
+ // Prevent diff content from actually being edited: Making the diff
+ // table content editable is just a mechanism to allow processing
+ // 'beforeInput' events, but the content itself should not be editable
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ }
+ }
+ },
+ true
+ );
+
+ window.addEventListener(
+ 'selectstart',
+ _ => {
+ contentEditableRange = null;
+ },
+ true
+ );
+}
+
+export function getContentEditableRange() {
+ return contentEditableRange;
+}
diff --git a/polygerrit-ui/app/yarn.lock b/polygerrit-ui/app/yarn.lock
index 543017a..fed42a4 100644
--- a/polygerrit-ui/app/yarn.lock
+++ b/polygerrit-ui/app/yarn.lock
@@ -472,11 +472,6 @@
dependencies:
tslib "^1.9.0"
-shadow-selection-polyfill@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/shadow-selection-polyfill/-/shadow-selection-polyfill-1.1.0.tgz#87eee5c3cd9c7296f9fec083ba6f4910b1fa6686"
- integrity sha512-ntz8P6DLEFpx7gikeXZ4gSi3APE2D+BP0rKnnaBzED+Lm8je8nkNcayy6kGWPEDWMFbtm+Yvd1ONFaXcsVWn2w==
-
tslib@^1.9.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"