Add custom user named query support

This parses user queries from the "queries" file in the user's ref in
the All-Users project.  The "queries" file format is a simple text file
with two tab separated columns: NAME and QUERY.  The named queries are
accessible via the new query:<name> operator.

Change-Id: I1dd12230fe7488164ae1711c6c2fd22ebcd14166
This commit is contained in:
Martin Fick 2015-03-30 12:44:51 -06:00
parent b6b6ee280a
commit 5e5a36f736
8 changed files with 311 additions and 8 deletions

View File

@ -0,0 +1,32 @@
= Gerrit Code Review - Named Queries
[[user-named-queries]]
== User Named Queries
It is possible to define named queries on a user level. To do
this, define the named queries in the `queries` file of
the user's account ref in the `All-Users` project. The user's
account ref is based on the user's account id which is an
integer. The account refs are sharded by the last two digits
(`+nn+`) in the refname, leading to refs of the format
`+refs/users/nn/accountid+`. The user's queries file is a
2 column tab delimited file. The left column represents the
name of the query, and the right column represents the query
expression represented by the name.
Example queries file:
----
# Name Query
#
selfapproved owner:self label:code-review+2,user=self
blocked label:code-review-2 OR label:verified-1
# Note below how to reference your own named queries in other named queries
ready label:code-review+2 label:verified+1 -query:blocked status:open
----
GERRIT
------
Part of link:index.html[Gerrit Code Review]
SEARCHBOX
---------

View File

@ -98,6 +98,12 @@ ownerin:'GROUP'::
+
Changes originally submitted by a user in 'GROUP'.
[[query]]
query:'NAME'::
+
Changes which match the current user's query named 'NAME'
(see link:user-named-queries.html[Named Queries]).
[[reviewer]]
reviewer:'USER', r:'USER'::
+

View File

@ -0,0 +1,72 @@
// 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.account;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.git.QueryList;
import com.google.gerrit.server.git.ValidationError;
import com.google.gerrit.server.git.VersionedMetaData;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
/** Named Queries for user accounts. */
public class VersionedAccountQueries extends VersionedMetaData {
private static final Logger log = LoggerFactory.getLogger(VersionedAccountQueries.class);
public static VersionedAccountQueries forUser(Account.Id id) {
return new VersionedAccountQueries(RefNames.refsUsers(id));
}
private final String ref;
private QueryList queryList;
private VersionedAccountQueries(String ref) {
this.ref = ref;
}
@Override
protected String getRefName() {
return ref;
}
public QueryList getQueryList() {
return queryList;
}
@Override
protected void onLoad() throws IOException, ConfigInvalidException {
ValidationError.Sink errors = new ValidationError.Sink() {
@Override
public void error(ValidationError error) {
log.error("Error parsing file " + QueryList.FILE_NAME + ": " +
error.getMessage());
}
};
queryList = QueryList.parse(readUTF8(QueryList.FILE_NAME), errors);
}
@Override
protected boolean onSave(CommitBuilder commit) throws IOException,
ConfigInvalidException {
throw new UnsupportedOperationException("Cannot yet save named queries");
}
}

View File

@ -0,0 +1,41 @@
// 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.git;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public class QueryList extends TabFile {
public static final String FILE_NAME = "queries";
protected final Map<String, String> queriesByName;
private QueryList(List<Row> queriesByName) {
this.queriesByName = toMap(queriesByName);
}
public static QueryList parse(String text, ValidationError.Sink errors)
throws IOException {
return new QueryList(parse(text, FILE_NAME, errors));
}
public String getQuery(String name) {
return queriesByName.get(name);
}
public String asText() {
return asText("Name", "Query", queriesByName);
}
}

View File

@ -29,7 +29,7 @@ public class BasicChangeRewrites extends QueryRewriter<ChangeData> {
new InvalidProvider<InternalChangeQuery>(),
new InvalidProvider<ChangeQueryRewriter>(),
null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null));
null, null, null, null, null, null, null, null, null));
private static final QueryRewriter.Definition<ChangeData, BasicChangeRewrites> mydef =
new QueryRewriter.Definition<>(BasicChangeRewrites.class, BUILDER);

View File

@ -34,8 +34,11 @@ import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.account.VersionedAccountQueries;
import com.google.gerrit.server.change.ChangeTriplet;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AllUsersNameProvider;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.git.GitRepositoryManager;
@ -57,9 +60,13 @@ import com.google.inject.Provider;
import com.google.inject.ProvisionException;
import com.google.inject.util.Providers;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Repository;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
@ -108,6 +115,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
public static final String FIELD_PATH = "path";
public static final String FIELD_PROJECT = "project";
public static final String FIELD_PROJECTS = "projects";
public static final String FIELD_QUERY = "query";
public static final String FIELD_REF = "ref";
public static final String FIELD_REVIEWER = "reviewer";
public static final String FIELD_REVIEWERIN = "reviewerin";
@ -139,6 +147,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
final AccountResolver accountResolver;
final GroupBackend groupBackend;
final AllProjectsName allProjectsName;
final AllUsersNameProvider allUsersName;
final PatchListCache patchListCache;
final GitRepositoryManager repoManager;
final ProjectCache projectCache;
@ -166,6 +175,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
AccountResolver accountResolver,
GroupBackend groupBackend,
AllProjectsName allProjectsName,
AllUsersNameProvider allUsersName,
PatchListCache patchListCache,
GitRepositoryManager repoManager,
ProjectCache projectCache,
@ -178,9 +188,9 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
this(db, queryProvider, rewriter, userFactory, self,
capabilityControlFactory, changeControlGenericFactory,
changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
allProjectsName, patchListCache, repoManager, projectCache,
listChildProjects, indexes, submitStrategyFactory, conflictsCache,
trackingFooters,
allProjectsName, allUsersName, patchListCache, repoManager,
projectCache, listChildProjects, indexes, submitStrategyFactory,
conflictsCache, trackingFooters,
cfg == null ? true : cfg.getBoolean("change", "allowDrafts", true));
}
@ -198,6 +208,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
AccountResolver accountResolver,
GroupBackend groupBackend,
AllProjectsName allProjectsName,
AllUsersNameProvider allUsersName,
PatchListCache patchListCache,
GitRepositoryManager repoManager,
ProjectCache projectCache,
@ -220,6 +231,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
this.accountResolver = accountResolver;
this.groupBackend = groupBackend;
this.allProjectsName = allProjectsName;
this.allUsersName = allUsersName;
this.patchListCache = patchListCache;
this.repoManager = repoManager;
this.projectCache = projectCache;
@ -236,9 +248,9 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
Providers.of(otherUser),
capabilityControlFactory, changeControlGenericFactory,
changeDataFactory, fillArgs, plcUtil, accountResolver, groupBackend,
allProjectsName, patchListCache, repoManager, projectCache,
listChildProjects, indexes, submitStrategyFactory, conflictsCache,
trackingFooters, allowsDrafts);
allProjectsName, allUsersName, patchListCache, repoManager,
projectCache, listChildProjects, indexes, submitStrategyFactory,
conflictsCache, trackingFooters, allowsDrafts);
}
Arguments asUser(Account.Id otherId) {
@ -773,6 +785,25 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
return Predicate.or(owner(ownerIds), commentby(ownerIds));
}
@Operator
public Predicate<ChangeData> query(String name) throws QueryParseException {
AllUsersName allUsers = args.allUsersName.get();
try (Repository git = args.repoManager.openRepository(allUsers)) {
VersionedAccountQueries q = VersionedAccountQueries.forUser(self());
q.load(git);
String query = q.getQueryList().getQuery(name);
if (query != null) {
return parse(query);
}
} catch (RepositoryNotFoundException e) {
throw new QueryParseException("Unknown named query (no " +
allUsers.get() +" repo): " + name, e);
} catch (IOException | ConfigInvalidException e) {
throw new QueryParseException("Error parsing named query: " + name, e);
}
throw new QueryParseException("Unknown named query: " + name);
}
@Override
protected Predicate<ChangeData> defaultField(String query) throws QueryParseException {
if (query.startsWith("refs/")) {

View File

@ -0,0 +1,121 @@
// 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.git;
import static org.easymock.EasyMock.createNiceMock;
import static org.easymock.EasyMock.replay;
import static com.google.common.truth.Truth.assertThat;
import junit.framework.TestCase;
import org.junit.Test;
import java.io.IOException;
public class QueryListTest extends TestCase {
public static final String Q_P = "project:foo";
public static final String Q_B = "branch:bar";
public static final String Q_COMPLEX = "branch:bar AND peers:'is:open\t'";
public static final String N_FOO = "foo";
public static final String N_BAR = "bar";
public static final String L_FOO = N_FOO + "\t" + Q_P + "\n";
public static final String L_BAR = N_BAR + "\t" + Q_B + "\n";
public static final String L_FOO_PROP = N_FOO + " \t" + Q_P + "\n";
public static final String L_BAR_PROP = N_BAR + " \t" + Q_B + "\n";
public static final String L_FOO_PAD_F = " " + N_FOO + "\t" + Q_P + "\n";
public static final String L_FOO_PAD_E = N_FOO + " \t" + Q_P + "\n";
public static final String L_BAR_PAD_F = N_BAR + "\t " + Q_B + "\n";
public static final String L_BAR_PAD_E = N_BAR + "\t" + Q_B + " \n";
public static final String L_COMPLEX = N_FOO + "\t" + Q_COMPLEX + "\t \n";
public static final String L_BAD = N_FOO + "\n";
public static final String HEADER = "# Name\tQuery\n";
public static final String C1 = "# A Simple Comment\n";
public static final String C2 = "# Comment with a tab\t and multi # # #\n";
public static final String F_SIMPLE = L_FOO + L_BAR;
public static final String F_PROPER = L_BAR_PROP + L_FOO_PROP; // alpha order
public static final String F_PAD_F = L_FOO_PAD_F + L_BAR_PAD_F;
public static final String F_PAD_E = L_FOO_PAD_E + L_BAR_PAD_E;
@Test
public void testParseSimple() throws Exception {
QueryList ql = QueryList.parse(F_SIMPLE, null);
assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
}
@Test
public void testParseWHeader() throws Exception {
QueryList ql = QueryList.parse(HEADER + F_SIMPLE, null);
assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
}
@Test
public void testParseWComments() throws Exception {
QueryList ql = QueryList.parse(C1 + F_SIMPLE + C2, null);
assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
}
@Test
public void testParseFooComment() throws Exception {
QueryList ql = QueryList.parse("#" + L_FOO + L_BAR, null);
assertThat(ql.getQuery(N_FOO)).isNull();
assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
}
@Test
public void testParsePaddedFronts() throws Exception {
QueryList ql = QueryList.parse(F_PAD_F, null);
assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
}
@Test
public void testParsePaddedEnds() throws Exception {
QueryList ql = QueryList.parse(F_PAD_E, null);
assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_P);
assertThat(ql.getQuery(N_BAR)).isEqualTo(Q_B);
}
@Test
public void testParseComplex() throws Exception {
QueryList ql = QueryList.parse(L_COMPLEX, null);
assertThat(ql.getQuery(N_FOO)).isEqualTo(Q_COMPLEX);
}
@Test(expected = IOException.class)
public void testParseBad() throws Exception {
ValidationError.Sink sink = createNiceMock(ValidationError.Sink.class);
replay(sink);
QueryList.parse(L_BAD, sink);
}
@Test
public void testAsText() throws Exception {
String expectedText = HEADER + "#\n" + F_PROPER;
QueryList ql = QueryList.parse(F_SIMPLE, null);
String asText = ql.asText();
assertThat(asText).isEqualTo(expectedText);
ql = QueryList.parse(asText, null);
asText = ql.asText();
assertThat(asText).isEqualTo(expectedText);
}
}

View File

@ -27,7 +27,7 @@ public class FakeQueryBuilder extends ChangeQueryBuilder {
FakeQueryBuilder.class),
new ChangeQueryBuilder.Arguments(null, null, null, null, null, null,
null, null, null, null, null, null, null, null, null, null, null,
indexes, null, null, null, null));
null, indexes, null, null, null, null));
}
@Operator