Merge "Refactor the keyboard handling a little bit"
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 377012a..a2dc31f 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -251,6 +251,16 @@
often combined with 'branch:' and 'project:' operators to select
all related changes in a series.
+[[inhashtag]]
+inhashtag:'HASHTAG'::
++
+Changes where any hashtag contains 'HASHTAG', using a full-text search.
++
+If 'HASHTAG' starts with `^` it matches hashtag names by regular
+expression patterns. The
+link:http://www.brics.dk/automaton/[dk.brics.automaton
+library,role=external,window=_blank] is used for evaluation of such patterns.
+
[[hashtag]]
hashtag:'HASHTAG'::
+
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index c1b2dc7..9a0ae75 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -179,6 +179,11 @@
exact(ChangeQueryBuilder.FIELD_HASHTAG)
.buildRepeatable(cd -> cd.hashtags().stream().map(String::toLowerCase).collect(toSet()));
+ /** Hashtags as fulltext field for in-string search. */
+ public static final FieldDef<ChangeData, Iterable<String>> FUZZY_HASHTAG =
+ fullText("hashtag2")
+ .buildRepeatable(cd -> cd.hashtags().stream().map(String::toLowerCase).collect(toSet()));
+
/** Hashtags with original case. */
public static final FieldDef<ChangeData, Iterable<byte[]>> HASHTAG_CASE_AWARE =
storedOnly("_hashtag")
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index 969b071..ffccb51 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -135,9 +135,14 @@
new Schema.Builder<ChangeData>().add(V59).add(ChangeField.MERGE).build();
/** Added new field {@link ChangeField#MERGED_ON} */
+ @Deprecated
static final Schema<ChangeData> V61 =
new Schema.Builder<ChangeData>().add(V60).add(ChangeField.MERGED_ON).build();
+ /** Added new field {@link ChangeField#FUZZY_HASHTAG} */
+ static final Schema<ChangeData> V62 =
+ new Schema.Builder<ChangeData>().add(V61).add(ChangeField.FUZZY_HASHTAG).build();
+
/**
* Name of the change index to be used when contacting index backends or loading configurations.
*/
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 4e3edcd..6e2f49c 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -790,7 +790,23 @@
@Operator
public Predicate<ChangeData> hashtag(String hashtag) {
- return new HashtagPredicate(hashtag);
+ return new ExactHashtagPredicate(hashtag);
+ }
+
+ @Operator
+ public Predicate<ChangeData> inhashtag(String hashtag) throws QueryParseException {
+ if (hashtag.startsWith("^")) {
+ return new RegexHashtagPredicate(hashtag);
+ }
+ if (hashtag.isEmpty()) {
+ return new ExactHashtagPredicate(hashtag);
+ }
+
+ if (!args.index.getSchema().hasField(ChangeField.FUZZY_HASHTAG)) {
+ throw new QueryParseException(
+ "'inhashtag' operator is not supported by change index version");
+ }
+ return new FuzzyHashtagPredicate(hashtag, args.index);
}
@Operator
diff --git a/java/com/google/gerrit/server/query/change/HashtagPredicate.java b/java/com/google/gerrit/server/query/change/ExactHashtagPredicate.java
similarity index 77%
rename from java/com/google/gerrit/server/query/change/HashtagPredicate.java
rename to java/com/google/gerrit/server/query/change/ExactHashtagPredicate.java
index 1fe4af4..a6526f7 100644
--- a/java/com/google/gerrit/server/query/change/HashtagPredicate.java
+++ b/java/com/google/gerrit/server/query/change/ExactHashtagPredicate.java
@@ -14,19 +14,23 @@
package com.google.gerrit.server.query.change;
+import com.google.common.base.Strings;
import com.google.gerrit.server.change.HashtagsUtil;
import com.google.gerrit.server.index.change.ChangeField;
-public class HashtagPredicate extends ChangeIndexPredicate {
- public HashtagPredicate(String hashtag) {
+public class ExactHashtagPredicate extends ChangeIndexPredicate {
+ public ExactHashtagPredicate(String hashtag) {
// Use toLowerCase without locale to match behavior in ChangeField.
// TODO(dborowitz): Change both.
super(ChangeField.HASHTAG, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase());
}
@Override
- public boolean match(ChangeData object) {
- for (String hashtag : object.notes().load().getHashtags()) {
+ public boolean match(ChangeData cd) {
+ if (Strings.isNullOrEmpty(getValue())) {
+ return cd.hashtags().isEmpty();
+ }
+ for (String hashtag : cd.hashtags()) {
if (hashtag.equalsIgnoreCase(getValue())) {
return true;
}
diff --git a/java/com/google/gerrit/server/query/change/FuzzyHashtagPredicate.java b/java/com/google/gerrit/server/query/change/FuzzyHashtagPredicate.java
new file mode 100644
index 0000000..35c96ef
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/FuzzyHashtagPredicate.java
@@ -0,0 +1,38 @@
+// 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 static com.google.gerrit.server.index.change.ChangeField.FUZZY_HASHTAG;
+
+import com.google.gerrit.server.index.change.ChangeIndex;
+
+public class FuzzyHashtagPredicate extends ChangeIndexPredicate {
+ protected final ChangeIndex index;
+
+ public FuzzyHashtagPredicate(String hashtag, ChangeIndex index) {
+ super(FUZZY_HASHTAG, hashtag.toLowerCase());
+ this.index = index;
+ }
+
+ @Override
+ 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/RegexHashtagPredicate.java b/java/com/google/gerrit/server/query/change/RegexHashtagPredicate.java
new file mode 100644
index 0000000..24efa6a
--- /dev/null
+++ b/java/com/google/gerrit/server/query/change/RegexHashtagPredicate.java
@@ -0,0 +1,51 @@
+// 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 static com.google.gerrit.server.index.change.ChangeField.HASHTAG;
+
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+public class RegexHashtagPredicate extends ChangeRegexPredicate {
+ protected final RunAutomaton pattern;
+
+ public RegexHashtagPredicate(String re) {
+ super(HASHTAG, re);
+
+ if (re.startsWith("^")) {
+ re = re.substring(1);
+ }
+
+ if (re.endsWith("$") && !re.endsWith("\\$")) {
+ re = re.substring(0, re.length() - 1);
+ }
+
+ this.pattern = new RunAutomaton(new RegExp(re).toAutomaton());
+ }
+
+ @Override
+ public boolean match(ChangeData cd) {
+ if (cd.hashtags().isEmpty()) {
+ return false;
+ }
+ return cd.hashtags().stream().anyMatch(ht -> pattern.run(ht));
+ }
+
+ @Override
+ public int getCost() {
+ return 1;
+ }
+}
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index cbeb59d..de9c0a5 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -1951,16 +1951,17 @@
Change change1 = insert(repo, newChange(repo));
Change change2 = insert(repo, newChange(repo));
- HashtagsInput in = new HashtagsInput();
- in.add = ImmutableSet.of("foo");
- gApi.changes().id(change1.getId().get()).setHashtags(in);
-
- in.add = ImmutableSet.of("foo", "bar", "a tag", "ACamelCaseTag");
- gApi.changes().id(change2.getId().get()).setHashtags(in);
-
+ addHashtags(change1.getId(), "foo", "aaa-bbb-ccc");
+ addHashtags(change2.getId(), "foo", "bar", "a tag", "ACamelCaseTag");
return ImmutableList.of(change1, change2);
}
+ private void addHashtags(Change.Id changeId, String... hashtags) throws Exception {
+ HashtagsInput in = new HashtagsInput();
+ in.add = ImmutableSet.copyOf(hashtags);
+ gApi.changes().id(changeId.get()).setHashtags(in);
+ }
+
@Test
public void byHashtag() throws Exception {
List<Change> changes = setUpHashtagChanges();
@@ -1976,6 +1977,31 @@
}
@Test
+ public void byHashtagFullText() throws Exception {
+ assume().that(getSchema().hasField(ChangeField.FUZZY_HASHTAG)).isTrue();
+ List<Change> changes = setUpHashtagChanges();
+ assertQuery("inhashtag:foo", changes.get(1), changes.get(0));
+ assertQuery("inhashtag:bbb", changes.get(0));
+ assertQuery("inhashtag:tag", changes.get(1));
+ }
+
+ @Test
+ public void byHashtagRegex() throws Exception {
+ TestRepository<Repo> repo = createProject("repo");
+ Change change1 = insert(repo, newChange(repo));
+ Change change2 = insert(repo, newChange(repo));
+ Change change3 = insert(repo, newChange(repo));
+ addHashtags(change1.getId(), "feature1");
+ addHashtags(change1.getId(), "trending");
+ addHashtags(change2.getId(), "Cherrypick-feature1");
+ addHashtags(change3.getId(), "feature1-fixup");
+
+ assertQuery("inhashtag:^feature1.*", change3, change1);
+ assertQuery("inhashtag:{^.*feature1$}", change2, change1);
+ assertQuery("inhashtag:^trending.*", change1);
+ }
+
+ @Test
public void byDefault() throws Exception {
TestRepository<Repo> repo = createProject("repo");