Implement `hasfooter` predicate to search for changes with a footer ignoring it's value In the context of submit requirements, there is a use case to match changes that have a footer with a certain name, irrespective of the footer's value. The most prominent example is `hasfooter:Bug`. In the Gerrit project we are thinking of requiring changes to have a `Release-Notes` footer to make releasing easier. That requirement would also not need to care about the footer's value. This seems universally useful also for change search, so we implement it as change predicate instead of submit requirement predicate. Change-Id: I34a35fdab6fdc4e0f09aa0bec71af4fcdfbb2bbb
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt index f314e46..7e8a7e0 100644 --- a/Documentation/user-search.txt +++ b/Documentation/user-search.txt
@@ -419,6 +419,12 @@ current patch set. 'FOOTER' can be specified verbatim ('<key>: <value>', must be quoted) or as '<key>=<value>'. The matching is done case-insensitive. +[[hasfooter-operator]] +hasfooter:'FOOTERNAME':: ++ +Matches any change that has a commit message with a footer where the footer +name is equal to 'FOOTERNAME'.The matching is done case-sensitive. + [[star]] star:'LABEL':: +
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java index 3d5dca8..baac95b 100644 --- a/java/com/google/gerrit/server/index/change/ChangeField.java +++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -271,6 +271,14 @@ .collect(toSet()); } + /** Footers from the commit message of the current patch set. */ + public static final FieldDef<ChangeData, Iterable<String>> FOOTER_NAME = + exact(ChangeQueryBuilder.FIELD_FOOTER_NAME).buildRepeatable(ChangeField::getFootersNames); + + public static Set<String> getFootersNames(ChangeData cd) { + return cd.commitFooters().stream().map(f -> f.getKey()).collect(toSet()); + } + /** Folders that are touched by the current patch set. */ public static final FieldDef<ChangeData, Iterable<String>> DIRECTORY = exact(ChangeQueryBuilder.FIELD_DIRECTORY).buildRepeatable(ChangeField::getDirectories);
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java index 9ff806d..9776584 100644 --- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java +++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -205,6 +205,7 @@ * Added new field {@link ChangeField#PREFIX_HASHTAG} and {@link ChangeField#PREFIX_TOPIC} to * allow easier search for topics. */ + @Deprecated static final Schema<ChangeData> V75 = new Schema.Builder<ChangeData>() .add(V74) @@ -212,6 +213,10 @@ .add(ChangeField.PREFIX_TOPIC) .build(); + /** Added new field {@link ChangeField#FOOTER_NAME}. */ + static final Schema<ChangeData> V76 = + new Schema.Builder<ChangeData>().add(V75).add(ChangeField.FOOTER_NAME).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/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java index 355f9de..ce17b31 100644 --- a/java/com/google/gerrit/server/query/change/ChangePredicates.java +++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -270,6 +270,14 @@ } /** + * Returns a predicate that matches changes with the provided {@code footer} name in their commit + * message. + */ + public static Predicate<ChangeData> hasFooter(String footerName) { + return new ChangeIndexPredicate(ChangeField.FOOTER_NAME, footerName); + } + + /** * Returns a predicate that matches changes that modified files in the provided {@code directory}. */ public static Predicate<ChangeData> directory(String directory) {
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java index c828d4d..4491aa6 100644 --- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java +++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -163,6 +163,7 @@ public static final String FIELD_EXTENSION = "extension"; public static final String FIELD_ONLY_EXTENSIONS = "onlyextensions"; public static final String FIELD_FOOTER = "footer"; + public static final String FIELD_FOOTER_NAME = "footernames"; public static final String FIELD_CONFLICTS = "conflicts"; public static final String FIELD_DELETED = "deleted"; public static final String FIELD_DELTA = "delta"; @@ -955,6 +956,12 @@ } @Operator + public Predicate<ChangeData> hasfooter(String footerName) throws QueryParseException { + checkFieldAvailable(ChangeField.FOOTER_NAME, "hasfooter"); + return ChangePredicates.hasFooter(footerName); + } + + @Operator public Predicate<ChangeData> dir(String directory) { return directory(directory); }
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java index e916147..c851e64 100644 --- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java +++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -1675,6 +1675,27 @@ } @Test + public void byFooterName() throws Exception { + assume().that(getSchema().hasField(ChangeField.FOOTER_NAME)).isTrue(); + TestRepository<Repo> repo = createProject("repo"); + RevCommit commit1 = repo.parseBody(repo.commit().message("Test\n\nfoo: bar").create()); + Change change1 = insert(repo, newChangeForCommit(repo, commit1)); + RevCommit commit2 = repo.parseBody(repo.commit().message("Test\n\nBaR: baz").create()); + Change change2 = insert(repo, newChangeForCommit(repo, commit2)); + + // create a changes with lines that look like footers, but which are not + RevCommit commit6 = repo.parseBody(repo.commit().message("Test\n\na=b: c").create()); + insert(repo, newChangeForCommit(repo, commit6)); + + // matching by 'key=value' works + assertQuery("hasfooter:foo", change1); + + // case matters + assertQuery("hasfooter:BaR", change2); + assertQuery("hasfooter:Bar"); + } + + @Test public void byDirectory() throws Exception { TestRepository<Repo> repo = createProject("repo"); Change change1 = insert(repo, newChangeWithFiles(repo, "src/foo.h", "src/foo.cc"));