gerrit query SSH command

Define an SSH command that offers the ability to query for change
summary information.  This provides the same information as a listing
on the web interface.

The query command is also available over HTTP under the /query URL,
with the query string supplied as the q parameter.

Bug: issue 504
Change-Id: I72fc11dc8872b781bcd6895b3bcd90d85f22e419
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce
2010-07-19 09:44:46 -07:00
parent e96a2367b4
commit 14760b7c0e
20 changed files with 1020 additions and 67 deletions

View File

@@ -66,6 +66,9 @@ link:cmd-review.html[gerrit approve]::
link:cmd-ls-projects.html[gerrit ls-projects]::
List projects visible to the caller.
link:cmd-query.html[gerrit query]::
Query the change database.
link:cmd-review.html[gerrit review]::
Verify, approve and/or submit a patch set from the command line.

110
Documentation/cmd-query.txt Normal file
View File

@@ -0,0 +1,110 @@
gerrit query
============
NAME
----
gerrit query - Query the change database
SYNOPSIS
--------
[verse]
'ssh' -p <port> <host> 'gerrit query' \
[\--format {TEXT | JSON}] \
[\--current-patch-set] \
[\--patch-sets] \
[\--] \
<query> \
[limit:<n>] \
[resume\_sortkey:<sortKey>]
DESCRIPTION
-----------
Queries the change database and returns results describing changes
that match the input query. More recently updated changes appear
before older changes, which is the same order presented in the
web interface.
A query may be limited on the number of results it returns with the
'limit:' operator. If no limit is supplied an internal default
limit is used to prevent explosion of the result set. To obtain
results beyond the limit, the 'resume_sortkey:' operator can be used
to resume the query at the change that follows the last change of
the prior result set.
Non-option arguments to this command are joined with spaces and then
parsed as a query. This simplifies calling conventions over SSH
by permitting operators to appear in different arguments without
multiple levels of quoting required.
OPTIONS
-------
\--current-patch-set::
Include information about the current patch set in the results.
\--patch-sets::
Include information about all patch sets. If combined with
the \--current-patch-set flag then the current patch set
information will be output twice, once in each field.
limit:<n>::
Maximum number of results to return. This is actually a
query operator, and not a command line option. If more
than one limit: operator is provided, the smallest limit
will be used to cut the result set.
resume\_sortkey:<sortKey>::
Resume results from this sort key. Callers should pass
the sortKey of the last change of the prior result set to
resume a prior query. This is actually a query operator,
and not a command line option.
ACCESS
------
Any user who has configured an SSH key.
SCRIPTING
---------
This command is intended to be used in scripts.
EXAMPLES
--------
Find the 2 most recent open changes in the tools/gerrit project:
-----
$ ssh -p 29418 review.example.com gerrit query --format=JSON status:open project:tools/gerrit limit:2
{"project":"tools/gerrit", ...}
{"project":"tools/gerrit", ..., sortKey:"000e6aee00003e26", ...}
{"type":"stats","rowCount":2,"runningTimeMilliseconds:15}
-----
Resume the same query and obtain the final results:
-----
$ ssh -p 29418 review.example.com gerrit query --format=JSON status:open project:tools/gerrit limit:2 resume_sortkey:000e6aee00003e26
{"project":"tools/gerrit", ...}
{"project":"tools/gerrit", ...}
{"type":"stats","rowCount":1,"runningTimeMilliseconds:15}
-----
SCHEMA
------
The JSON messages consist of nested objects referencing the
link:json.html#change[change],
link:json.html#patchset[patchset],
link:json.html#[account]
involved, and other attributes as appropriate.
Note that any field may be missing in the JSON messages, so consumers
of this JSON stream should deal with that appropriately.
SEE ALSO
--------
* link:user-search.html[Query Operators]
* link:json.html[JSON Data Formats]
* link:access-control.html[Access Controls]
GERRIT
------
Part of link:index.html[Gerrit Code Review]

View File

@@ -54,93 +54,48 @@ Patchset Added
^^^^^^^^^^^^^^
type:: "patchset-added"
change:: <<change,change attribute>>
change:: link:json.html#change[change attribute]
patchset:: <<patchset,patchset attribute>>
patchset:: link:json.html#patchset[patchset attribute]
uploader:: <<account,account attribute>>
uploader:: link:json.html#account[account attribute]
Change Abandoned
^^^^^^^^^^^^^^^^
type:: "change-abandoned"
change:: <<change,change attribute>>
change:: link:json.html#change[change attribute]
patchset:: <<patchset,patchset attribute>>
patchset:: link:json.html#patchset[patchset attribute]
abandoner:: <<account,account attribute>>
abandoner:: link:json.html#account[account attribute]
Change Merged
^^^^^^^^^^^^^
type:: "change-merged"
change:: <<change,change attribute>>
change:: link:json.html#change[change attribute]
patchset:: <<patchset,patchset attribute>>
patchset:: link:json.html#patchset[patchset attribute]
submitter:: <<account,account attribute>>
submitter:: link:json.html#account[account attribute]
Comment Added
^^^^^^^^^^^^^
type:: "comment-added"
change:: <<change,change attribute>>
change:: link:json.html#change[change attribute]
patchset:: <<patchset,patchset attribute>>
patchset:: link:json.html#patchset[patchset attribute]
author:: <<account,account attribute>>
author:: link:json.html#account[account attribute]
comment:: Comment text author had written
Attributes
~~~~~~~~~~
Attributes are part events to give context related to the event.
[[change]]
change:: The Gerrit change the event is related to
project;; Project path in Gerrit
branch;; Branch name within project
topic;; Topic name specified by the uploaded for this change series
id;; Change identifier
number;; Change number (deprecated)
subject;; Description of change
owner;; Owner in account attribute
url;; Canonical URL to reach this change
lastUpdated;; Time in seconds since the UNIX epoch when this change
was last updated.
sortKey;; Internal key used to sort changes, based on lastUpdated.
[[account]]
account:: An account that is related to an event or attribute
name;; Account user's full name
email;; Account user's preferred email
[[patchset]]
patchset:: Refers to a specific patchset within a change
number;; The patchset number
revision;; Git commit-ish for this patchset
ref;; Git reference pointing at revision
uploader;; Uploader of patch set in account attribute
SEE ALSO
--------
* link:json.html[JSON Data Formats]
* link:access-control.html[Access Controls]
GERRIT

118
Documentation/json.txt Normal file
View File

@@ -0,0 +1,118 @@
Gerrit Code Review - JSON Data
==============================
Some commands produce JSON data streams intended for other
applications to consume. The structures are documented below.
Note that any field may be missing in the JSON messages, so consumers
of this JSON stream should deal with that appropriately.
[[change]]
change
------
The Gerrit change being reviewed, or that was already reviewed.
project:: Project path in Gerrit
branch:: Branch name within project
topic:: Topic name specified by the uploader for this change series
id:: Change identifier, as scraped out of the Change-Id field in
the commit message, or as assigned by the server if it was missing.
number:: Change number (deprecated)
subject:: Description of change
owner:: Owner in <<account,account attribute>>
url:: Canonical URL to reach this change
lastUpdated:: Time in seconds since the UNIX epoch when this change
was last updated.
sortKey:: Internal key used to sort changes, based on lastUpdated.
open:: Boolean indicating if the change is still open for review.
status:: Current state of this change.
NEW;; Change is still being reviewed.
SUBMITTED;; Change has been submitted and is in the merge queue.
It may be waiting for one or more dependencies.
MERGED;; Change has been merged to its branch.
ABANDONED;; Change was abandoned by its owner or administrator.
trackingIds:: Issue tracking system links in
<<trackingid,trackingid attribute>>, scraped out of the commit
message based on the server's
link:config-gerrit.html#trackingid[trackingid] sections.
currentPatchSet:: Current <<patchset,patchset attribute>>.
patchSets:: All <<patchset,patchset attribute>> for this change.
[[trackingid]]
trackingid
----------
A link to an issue tracking system.
system:: Name of the system. This comes straight from the
gerrit.config file.
id:: Id number as scraped out of the commit message.
[[account]]
account
-------
A user account.
name:: User's full name, if configured.
email:: User's preferred email address.
[[patchset]]
patchset
--------
Refers to a specific patchset within a <<change,change>>.
number:: The patchset number.
revision:: Git commit for this patchset.
ref:: Git reference pointing at the revision. This reference is
available through the Gerrit Code Review server's Git interface
for the containing change.
uploader:: Uploader of the patch set in <<account,account attribute>>.
approvals:: The <<approval,approval attribute>> granted.
[[approval]]
approval
--------
Records the code review approval granted to a patch set.
type:: Internal name of the approval given.
description:: Human readable category of the approval.
value:: Value assigned by the approval, usually a numerical score.
grantedOn:: Time in seconds since the UNIX epoch when this approval
was added or last updated.
by:: Reviewer of the patch set in <<account,account attribute>>.
SEE ALSO
--------
* link:cmd-stream-events.html[gerrit stream-events]
* link:cmd-query.html[gerrit query]
GERRIT
------
Part of link:index.html[Gerrit Code Review]

View File

@@ -334,6 +334,13 @@ automatically set to the page size configured in the current user's
preferences. Including it in a web query may lead to unpredictable
results with regards to pagination.
resume\_sortkey:'KEY'::
+
Positions the low level scan routine to start from 'KEY' and
continue through changes from this point. This is most often used
for paginating result sets. Including this in a web query may lead
to unpredictable results.
sortkey\_after:'KEY', sortkey\_before:'KEY'::
+
Restart the low level scan routine from 'KEY'. This is automatically

View File

@@ -0,0 +1,112 @@
// 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.httpd;
import com.google.gerrit.server.query.change.QueryProcessor;
import com.google.gerrit.server.query.change.QueryProcessor.OutputFormat;
import com.google.gson.Gson;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Singleton
public class ChangeQueryServlet extends HttpServlet {
private final Provider<QueryProcessor> processor;
@Inject
ChangeQueryServlet(Provider<QueryProcessor> processor) {
this.processor = processor;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse rsp)
throws IOException {
rsp.setContentType("text/json");
rsp.setCharacterEncoding("UTF-8");
QueryProcessor p = processor.get();
OutputFormat format = OutputFormat.JSON;
try {
format = OutputFormat.valueOf(get(req, "format", format.toString()));
} catch (IllegalArgumentException err) {
error(rsp, "invalid format");
return;
}
switch (format) {
case JSON:
rsp.setContentType("text/json");
rsp.setCharacterEncoding("UTF-8");
break;
case TEXT:
rsp.setContentType("text/plain");
rsp.setCharacterEncoding("UTF-8");
break;
default:
error(rsp, "invalid format");
return;
}
p.setIncludeCurrentPatchSet(get(req, "current-patch-set", false));
p.setIncludePatchSets(get(req, "patch-sets", false));
p.setOutput(rsp.getOutputStream(), format);
p.query(get(req, "q", "status:open"));
}
private static void error(HttpServletResponse rsp, String message)
throws IOException {
ErrorMessage em = new ErrorMessage();
em.message = message;
ServletOutputStream out = rsp.getOutputStream();
try {
out.write(new Gson().toJson(em).getBytes("UTF-8"));
out.write('\n');
out.flush();
} finally {
out.close();
}
}
private static String get(HttpServletRequest req, String name, String val) {
String v = req.getParameter(name);
if (v == null || v.isEmpty()) {
return val;
}
return v;
}
private static boolean get(HttpServletRequest req, String name, boolean val) {
String v = req.getParameter(name);
if (v == null || v.isEmpty()) {
return val;
}
return "true".equalsIgnoreCase(v);
}
public static class ErrorMessage {
public final String type = "error";
public String message;
}
}

View File

@@ -46,6 +46,7 @@ class UrlModule extends ServletModule {
serve("/Gerrit/*").with(legacyGerritScreen());
serve("/cat/*").with(CatServlet.class);
serve("/logout").with(HttpLogoutServlet.class);
serve("/query").with(ChangeQueryServlet.class);
serve("/signout").with(HttpLogoutServlet.class);
serve("/ssh_info").with(SshInfoServlet.class);
serve("/static/*").with(StaticServlet.class);

View File

@@ -18,4 +18,7 @@ public class ApprovalAttribute {
public String type;
public String description;
public String value;
public Long grantedOn;
public AccountAttribute by;
}

View File

@@ -14,6 +14,10 @@
package com.google.gerrit.server.events;
import com.google.gerrit.reviewdb.Change;
import java.util.List;
public class ChangeAttribute {
public String project;
public String branch;
@@ -23,6 +27,13 @@ public class ChangeAttribute {
public String subject;
public AccountAttribute owner;
public String url;
public long lastUpdated;
public Long lastUpdated;
public String sortKey;
public Boolean open;
public Change.Status status;
public List<TrackingIdAttribute> trackingIds;
public PatchSetAttribute currentPatchSet;
public List<PatchSetAttribute> patchSets;
}

View File

@@ -14,27 +14,36 @@
package com.google.gerrit.server.events;
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.PatchSet;
import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.TrackingId;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.internal.Nullable;
import java.util.ArrayList;
import java.util.Collection;
@Singleton
public class EventFactory {
private final AccountCache accountCache;
private final Provider<String> urlProvider;
private final ApprovalTypes approvalTypes;
@Inject
EventFactory(AccountCache accountCache,
@CanonicalWebUrl @Nullable Provider<String> urlProvider) {
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
ApprovalTypes approvalTypes) {
this.accountCache = accountCache;
this.urlProvider = urlProvider;
this.approvalTypes = approvalTypes;
}
/**
@@ -53,11 +62,45 @@ public class EventFactory {
a.number = change.getId().toString();
a.subject = change.getSubject();
a.url = getChangeUrl(change);
a.owner = asAccountAttribute(change.getOwner());
return a;
}
/**
* Extend the existing ChangeAttribute with additional fields.
*
* @param a
* @param change
*/
public void extend(ChangeAttribute a, Change change) {
a.lastUpdated = change.getLastUpdatedOn().getTime() / 1000L;
a.sortKey = change.getSortKey();
a.open = change.getStatus().isOpen();
a.status = change.getStatus();
}
final AccountState owner = accountCache.get(change.getOwner());
a.owner = asAccountAttribute(owner.getAccount());
public void addTrackingIds(ChangeAttribute a, Collection<TrackingId> ids) {
if (!ids.isEmpty()) {
a.trackingIds = new ArrayList<TrackingIdAttribute>(ids.size());
for (TrackingId t : ids) {
a.trackingIds.add(asTrackingIdAttribute(t));
}
}
}
public void addPatchSets(ChangeAttribute a, Collection<PatchSet> ps) {
if (!ps.isEmpty()) {
a.patchSets = new ArrayList<PatchSetAttribute>(ps.size());
for (PatchSet p : ps) {
a.patchSets.add(asPatchSetAttribute(p));
}
}
}
public TrackingIdAttribute asTrackingIdAttribute(TrackingId id) {
TrackingIdAttribute a = new TrackingIdAttribute();
a.system = id.getSystem();
a.id = id.getTrackingId();
return a;
}
@@ -73,12 +116,36 @@ public class EventFactory {
p.revision = patchSet.getRevision().get();
p.number = Integer.toString(patchSet.getPatchSetId());
p.ref = patchSet.getRefName();
final AccountState uploader = accountCache.get(patchSet.getUploader());
p.uploader = asAccountAttribute(uploader.getAccount());
p.uploader = asAccountAttribute(patchSet.getUploader());
return p;
}
public void addApprovals(PatchSetAttribute p,
Collection<PatchSetApproval> list) {
if (!list.isEmpty()) {
p.approvals = new ArrayList<ApprovalAttribute>(list.size());
for (PatchSetApproval a : list) {
if (a.getValue() != 0) {
p.approvals.add(asApprovalAttribute(a));
}
}
if (p.approvals.isEmpty()) {
p.approvals = null;
}
}
}
/**
* Create an AuthorAttribute for the given account suitable for serialization
* to JSON.
*
* @param id
* @return object suitable for serialization to JSON
*/
public AccountAttribute asAccountAttribute(Account.Id id) {
return asAccountAttribute(accountCache.get(id).getAccount());
}
/**
* Create an AuthorAttribute for the given account suitable for serialization
* to JSON.
@@ -93,6 +160,27 @@ public class EventFactory {
return who;
}
/**
* Create an ApprovalAttribute for the given approval suitable for
* serialization to JSON.
*
* @param approval
* @return object suitable for serialization to JSON
*/
public ApprovalAttribute asApprovalAttribute(PatchSetApproval approval) {
ApprovalAttribute a = new ApprovalAttribute();
a.type = approval.getCategoryId().get();
a.value = Short.toString(approval.getValue());
a.by = asAccountAttribute(approval.getAccountId());
a.grantedOn = approval.getGranted().getTime() / 1000L;
ApprovalType at = approvalTypes.getApprovalType(approval.getCategoryId());
if (at != null) {
a.description = at.getCategory().getName();
}
return a;
}
/** Get a link to the change; null if the server doesn't know its own address. */
private String getChangeUrl(final Change change) {
if (change != null && urlProvider.get() != null) {

View File

@@ -14,9 +14,13 @@
package com.google.gerrit.server.events;
import java.util.List;
public class PatchSetAttribute {
public String number;
public String revision;
public String ref;
public AccountAttribute uploader;
public List<ApprovalAttribute> approvals;
}

View File

@@ -0,0 +1,21 @@
// 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.events;
public class QueryStats {
public final String type = "stats";
public int rowCount;
public long runTimeMilliseconds;
}

View File

@@ -0,0 +1,20 @@
// 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.events;
public class TrackingIdAttribute {
public String system;
public String id;
}

View File

@@ -26,6 +26,8 @@ import static com.google.gerrit.server.query.QueryParser.OR;
import static com.google.gerrit.server.query.QueryParser.SINGLE_WORD;
import static com.google.gerrit.server.query.QueryParser.VARIABLE_ASSIGN;
import com.google.gerrit.server.query.change.ChangeData;
import org.antlr.runtime.tree.Tree;
import java.lang.annotation.ElementType;
@@ -237,6 +239,33 @@ public abstract class QueryBuilder<T> {
throw error("Unsupported query:" + value);
}
/**
* Locate a predicate in the predicate tree.
*
* @param p the predicate to find.
* @param clazz type of the predicate instance.
* @param name name of the operator.
* @return the predicate, null if not found.
*/
@SuppressWarnings("unchecked")
public <P extends OperatorPredicate<T>> P find(Predicate<T> p,
Class<P> clazz, String name) {
if (p instanceof OperatorPredicate
&& ((OperatorPredicate) p).getOperator().equals(name)
&& clazz.isAssignableFrom(p.getClass())) {
return (P) p;
}
for (Predicate<T> c : p.getChildren()) {
P r = find(c, clazz, name);
if (r != null) {
return r;
}
}
return null;
}
@SuppressWarnings("unchecked")
private Predicate<T>[] children(final Tree r) throws QueryParseException,
IllegalArgumentException {

View File

@@ -24,7 +24,10 @@ import com.google.gerrit.server.CurrentUser;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Provider;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
public class ChangeData {
private final Change.Id legacyId;
@@ -71,6 +74,39 @@ public class ChangeData {
return change;
}
public PatchSet currentPatchSet(Provider<ReviewDb> db) throws OrmException {
Change c = change(db);
if (c == null) {
return null;
}
for (PatchSet p : patches(db)) {
if (p.getId().equals(c.currentPatchSetId())) {
return p;
}
}
return null;
}
public Collection<PatchSetApproval> currentApprovals(Provider<ReviewDb> db)
throws OrmException {
Change c = change(db);
if (c == null) {
return Collections.emptyList();
}
return approvalsFor(db, c.currentPatchSetId());
}
public Collection<PatchSetApproval> approvalsFor(Provider<ReviewDb> db,
PatchSet.Id psId) throws OrmException {
List<PatchSetApproval> r = new ArrayList<PatchSetApproval>();
for (PatchSetApproval p : approvals(db)) {
if (p.getPatchSetId().equals(psId)) {
r.add(p);
}
}
return r;
}
public Collection<PatchSet> patches(Provider<ReviewDb> db)
throws OrmException {
if (patches == null) {

View File

@@ -70,6 +70,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
public static final String FIELD_IS = "is";
public static final String FIELD_HAS = "has";
public static final String FIELD_LABEL = "label";
public static final String FIELD_LIMIT = "limit";
public static final String FIELD_OWNER = "owner";
public static final String FIELD_PROJECT = "project";
public static final String FIELD_REF = "ref";
@@ -334,7 +335,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
}
public Predicate<ChangeData> limit(int limit) {
return new IntPredicate<ChangeData>("limit", limit) {
return new IntPredicate<ChangeData>(FIELD_LIMIT, limit) {
@Override
public boolean match(ChangeData object) {
return true;
@@ -357,6 +358,22 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
return new SortKeyPredicate.Before(dbProvider, sortKey);
}
@Operator
public Predicate<ChangeData> resume_sortkey(String sortKey) {
return sortkey_before(sortKey);
}
@SuppressWarnings("unchecked")
public boolean hasLimit(Predicate<ChangeData> p) {
return find(p, IntPredicate.class, FIELD_LIMIT) != null;
}
@SuppressWarnings("unchecked")
public boolean hasSortKey(Predicate<ChangeData> p) {
return find(p, SortKeyPredicate.class, "sortkey_after") != null
|| find(p, SortKeyPredicate.class, "sortkey_before") != null;
}
@SuppressWarnings("unchecked")
@Override
protected Predicate<ChangeData> defaultField(String query)

View File

@@ -105,6 +105,23 @@ public class ChangeQueryRewriter extends QueryRewriter<ChangeData> {
return a.intValue() <= b.intValue() ? a : b;
}
@SuppressWarnings("unchecked")
@NoCostComputation
@Rewrite("A=(sortkey_before:*) B=(sortkey_before:*)")
public Predicate<ChangeData> r00_oldestSortKey(
@Named("A") SortKeyPredicate.Before a,
@Named("B") SortKeyPredicate.Before b) {
return a.getValue().compareTo(b.getValue()) <= 0 ? a : b;
}
@SuppressWarnings("unchecked")
@NoCostComputation
@Rewrite("A=(sortkey_after:*) B=(sortkey_after:*)")
public Predicate<ChangeData> r00_newestSortKey(
@Named("A") SortKeyPredicate.After a, @Named("B") SortKeyPredicate.After b) {
return a.getValue().compareTo(b.getValue()) >= 0 ? a : b;
}
@Rewrite("status:open P=(project:*) S=(sortkey_after:*) L=(limit:*)")
public Predicate<ChangeData> r10_byProjectOpenPrev(
@Named("P") final ProjectPredicate p,

View File

@@ -0,0 +1,329 @@
// 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.reviewdb.Change;
import com.google.gerrit.reviewdb.PatchSet;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.events.ChangeAttribute;
import com.google.gerrit.server.events.EventFactory;
import com.google.gerrit.server.events.QueryStats;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gson.Gson;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
public class QueryProcessor {
private static final Logger log =
LoggerFactory.getLogger(QueryProcessor.class);
public static enum OutputFormat {
TEXT, JSON;
}
private final Gson gson = new Gson();
private final SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz");
private final CurrentUser currentUser;
private final EventFactory eventFactory;
private final ChangeQueryBuilder queryBuilder;
private final ChangeQueryRewriter queryRewriter;
private final Provider<ReviewDb> db;
private int defaultLimit = 500;
private OutputFormat outputFormat = OutputFormat.TEXT;
private boolean includePatchSets;
private boolean includeCurrentPatchSet;
private OutputStream outputStream = DisabledOutputStream.INSTANCE;
private PrintWriter out;
@Inject
QueryProcessor(CurrentUser currentUser, EventFactory eventFactory,
ChangeQueryBuilder queryBuilder, ChangeQueryRewriter queryRewriter,
Provider<ReviewDb> db) {
this.currentUser = currentUser;
this.eventFactory = eventFactory;
this.queryBuilder = queryBuilder;
this.queryRewriter = queryRewriter;
this.db = db;
}
public void setIncludePatchSets(boolean on) {
includePatchSets = on;
}
public void setIncludeCurrentPatchSet(boolean on) {
includeCurrentPatchSet = on;
}
public void setOutput(OutputStream out, OutputFormat fmt) {
this.outputStream = out;
this.outputFormat = fmt;
}
public void query(String queryString) throws IOException {
out = new PrintWriter( //
new BufferedWriter( //
new OutputStreamWriter(outputStream, "UTF-8")));
try {
try {
final QueryStats stats = new QueryStats();
stats.runTimeMilliseconds = System.currentTimeMillis();
final Predicate<ChangeData> visibleToMe =
queryBuilder.visibleto(currentUser);
Predicate<ChangeData> s = compileQuery(queryString, visibleToMe);
List<ChangeData> results = new ArrayList<ChangeData>();
HashSet<Change.Id> want = new HashSet<Change.Id>();
for (ChangeData d : ((ChangeDataSource) s).read()) {
if (d.hasChange()) {
// Checking visibleToMe here should be unnecessary, the
// query should have already performed it. But we don't
// want to trust the query rewriter that much yet.
//
if (visibleToMe.match(d)) {
results.add(d);
}
} else {
want.add(d.getId());
}
}
if (!want.isEmpty()) {
for (Change c : db.get().changes().get(want)) {
ChangeData d = new ChangeData(c);
if (visibleToMe.match(d)) {
results.add(d);
}
}
}
Collections.sort(results, new Comparator<ChangeData>() {
@Override
public int compare(ChangeData a, ChangeData b) {
return b.getChange().getSortKey().compareTo(
a.getChange().getSortKey());
}
});
if (defaultLimit < results.size()) {
results = results.subList(0, defaultLimit);
}
for (ChangeData d : results) {
ChangeAttribute c = eventFactory.asChangeAttribute(d.getChange());
eventFactory.extend(c, d.getChange());
eventFactory.addTrackingIds(c, d.trackingIds(db));
if (includePatchSets) {
eventFactory.addPatchSets(c, d.patches(db));
}
if (includeCurrentPatchSet) {
PatchSet current = d.currentPatchSet(db);
if (current != null) {
c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
eventFactory.addApprovals(c.currentPatchSet, //
d.approvalsFor(db, current.getId()));
}
}
show(c);
}
stats.rowCount = results.size();
stats.runTimeMilliseconds =
System.currentTimeMillis() - stats.runTimeMilliseconds;
show(stats);
} catch (OrmException err) {
log.error("Cannot execute query: " + queryString, err);
ErrorMessage m = new ErrorMessage();
m.message = "cannot query database";
show(m);
} catch (QueryParseException e) {
ErrorMessage m = new ErrorMessage();
m.message = e.getMessage();
show(m);
}
} finally {
try {
out.flush();
} finally {
out = null;
}
}
}
@SuppressWarnings("unchecked")
private Predicate<ChangeData> compileQuery(String queryString,
final Predicate<ChangeData> visibleToMe) throws QueryParseException {
Predicate<ChangeData> q = queryBuilder.parse(queryString);
if (!queryBuilder.hasLimit(q)) {
q = Predicate.and(q, queryBuilder.limit(defaultLimit));
}
if (!queryBuilder.hasSortKey(q)) {
q = Predicate.and(q, queryBuilder.sortkey_before("z"));
}
q = Predicate.and(q, visibleToMe);
Predicate<ChangeData> s = queryRewriter.rewrite(q);
if (!(s instanceof ChangeDataSource)) {
s = queryRewriter.rewrite(Predicate.and(queryBuilder.status_open(), q));
}
if (!(s instanceof ChangeDataSource)) {
throw new QueryParseException("cannot execute query: " + s);
}
return s;
}
private void show(Object data) {
switch (outputFormat) {
default:
case TEXT:
if (data instanceof ChangeAttribute) {
out.print("change ");
out.print(((ChangeAttribute) data).id);
out.print("\n");
showText(data, 1);
} else {
showText(data, 0);
}
out.print('\n');
break;
case JSON:
out.print(gson.toJson(data));
out.print('\n');
break;
}
}
private void showText(Object data, int depth) {
for (Field f : fieldsOf(data.getClass())) {
Object val;
try {
val = f.get(data);
} catch (IllegalArgumentException err) {
continue;
} catch (IllegalAccessException err) {
continue;
}
if (val == null) {
continue;
}
indent(depth);
out.print(f.getName());
out.print(":");
if (val instanceof Long && isDateField(f.getName())) {
out.print(' ');
out.print(sdf.format(new Date(((Long) val) * 1000L)));
out.print('\n');
} else {
showTextValue(val, depth);
}
}
}
private void indent(int depth) {
for (int i = 0; i < depth; i++) {
out.print(" ");
}
}
@SuppressWarnings( {"cast", "unchecked"})
private void showTextValue(Object value, int depth) {
if (isPrimitive(value)) {
out.print(' ');
out.print(value);
out.print('\n');
} else if (value instanceof Collection) {
out.print('\n');
for (Object thing : ((Collection) value)) {
if (isPrimitive(thing)) {
out.print(' ');
out.print(value);
out.print('\n');
} else {
showText(thing, depth + 1);
out.print('\n');
}
}
} else {
out.print('\n');
showText(value, depth + 1);
}
}
@SuppressWarnings("unchecked")
private static boolean isPrimitive(Object value) {
return value instanceof String //
|| value instanceof Number //
|| value instanceof Boolean //
|| value instanceof Enum;
}
private static boolean isDateField(String name) {
return "lastUpdated".equals(name) //
|| "grantedOn".equals(name);
}
private List<Field> fieldsOf(Class<?> type) {
List<Field> r = new ArrayList<Field>();
if (type.getSuperclass() != null) {
r.addAll(fieldsOf(type.getSuperclass()));
}
r.addAll(Arrays.asList(type.getDeclaredFields()));
return r;
}
static class ErrorMessage {
public final String type = "error";
public String message;
}
}

View File

@@ -36,6 +36,7 @@ public class DefaultCommandModule extends CommandModule {
command(gerrit).toProvider(new DispatchCommandProvider(gerrit));
command(gerrit, "flush-caches").to(AdminFlushCaches.class);
command(gerrit, "ls-projects").to(ListProjects.class);
command(gerrit, "query").to(Query.class);
command(gerrit, "show-caches").to(AdminShowCaches.class);
command(gerrit, "show-connections").to(AdminShowConnections.class);
command(gerrit, "show-queue").to(ShowQueue.class);

View File

@@ -0,0 +1,71 @@
// 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.sshd.commands;
import com.google.gerrit.server.query.change.QueryProcessor;
import com.google.gerrit.sshd.BaseCommand;
import com.google.inject.Inject;
import org.apache.sshd.server.Environment;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
import java.util.List;
class Query extends BaseCommand {
@Inject
private QueryProcessor processor;
@Option(name = "--format", metaVar = "FMT", usage = "Output display format")
void setFormat(QueryProcessor.OutputFormat format) {
processor.setOutput(out, format);
}
@Option(name = "--current-patch-set", usage = "Include information about current patch set")
void setCurrentPatchSet(boolean on) {
processor.setIncludeCurrentPatchSet(on);
}
@Option(name = "--patch-sets", usage = "Include information about all patch sets")
void setPatchSets(boolean on) {
processor.setIncludePatchSets(on);
}
@Argument(index = 0, required = true, multiValued = true, metaVar = "QUERY", usage = "Query to execute")
private List<String> query;
@Override
public void start(Environment env) {
startThread(new CommandRunnable() {
@Override
public void run() throws Exception {
processor.setOutput(out, QueryProcessor.OutputFormat.TEXT);
parseCommandLine();
processor.query(join(query, " "));
}
});
}
private static String join(List<String> list, String sep) {
StringBuilder r = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
if (i > 0) {
r.append(sep);
}
r.append(list.get(i));
}
return r.toString();
}
}