Merge changes from topics 'query-refactor', 'kill-sortkey'

* changes:
  QueryProcessor: Don't double-add visibleto predicate
  Add query tests for visibleto predicate
  Terminate sortkey with prejudice
  Use secondary index for advertising extra haves during push
  OutputStreamQuery: Optimize formatter allocation
  Move stream-based QueryProcessor output to its own class
  Add QueryProcessor methods to search by Predicate
  Fix limit handling in QueryProcessor
  QueryChanges: Remove unused reverse field
This commit is contained in:
Shawn Pearce
2014-12-24 17:19:57 +00:00
committed by Gerrit Code Review
39 changed files with 826 additions and 639 deletions

View File

@@ -116,7 +116,7 @@ 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 $ ssh -p 29418 review.example.com gerrit query --format=JSON status:open project:tools/gerrit limit:2
{"project":"tools/gerrit", ...} {"project":"tools/gerrit", ...}
{"project":"tools/gerrit", ..., sortKey:"000e6aee00003e26", ...} {"project":"tools/gerrit", ...}
{"type":"stats","rowCount":2,"runningTimeMilliseconds:15} {"type":"stats","rowCount":2,"runningTimeMilliseconds:15}
==== ====

View File

@@ -35,8 +35,6 @@ was created.
lastUpdated:: Time in seconds since the UNIX epoch when this change lastUpdated:: Time in seconds since the UNIX epoch when this change
was last updated. was last updated.
sortKey:: Internal key used to sort changes, based on lastUpdated.
open:: Boolean indicating if the change is still open for review. open:: Boolean indicating if the change is still open for review.
status:: Current state of this change. status:: Current state of this change.

View File

@@ -1123,7 +1123,6 @@ link:rest-api-changes.html#change-info[ChangeInfo] entities.
"created": "2013-02-01 09:59:32.126000000", "created": "2013-02-01 09:59:32.126000000",
"updated": "2013-02-21 11:16:36.775000000", "updated": "2013-02-21 11:16:36.775000000",
"mergeable": true, "mergeable": true,
"_sortkey": "0023412400000f7d",
"_number": 3965, "_number": 3965,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"

View File

@@ -55,7 +55,6 @@ the resulting change.
"mergeable": true, "mergeable": true,
"insertions": 0, "insertions": 0,
"deletions": 0, "deletions": 0,
"_sortkey": "002cbc25000004e5",
"_number": 4711, "_number": 4711,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"
@@ -105,7 +104,6 @@ Query for open changes of watched projects:
"mergeable": true, "mergeable": true,
"insertions": 26, "insertions": 26,
"deletions": 10, "deletions": 10,
"_sortkey": "001e7057000006dc",
"_number": 1756, "_number": 1756,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"
@@ -123,7 +121,6 @@ Query for open changes of watched projects:
"mergeable": true, "mergeable": true,
"insertions": 12, "insertions": 12,
"deletions": 18, "deletions": 18,
"_sortkey": "001e7056000006dd",
"_number": 1757, "_number": 1757,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"
@@ -177,7 +174,6 @@ Query that retrieves changes for a user's dashboard:
"mergeable": true, "mergeable": true,
"insertions": 4, "insertions": 4,
"deletions": 7, "deletions": 7,
"_sortkey": "001e7057000006dc",
"_number": 1756, "_number": 1756,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"
@@ -330,7 +326,6 @@ default. Optional fields are:
"mergeable": true, "mergeable": true,
"insertions": 16, "insertions": 16,
"deletions": 7, "deletions": 7,
"_sortkey": "001c9bf400000061",
"_number": 97, "_number": 97,
"owner": { "owner": {
"name": "Shawn Pearce" "name": "Shawn Pearce"
@@ -467,7 +462,6 @@ describes the change.
"mergeable": true, "mergeable": true,
"insertions": 34, "insertions": 34,
"deletions": 101, "deletions": 101,
"_sortkey": "0023412400000f7d",
"_number": 3965, "_number": 3965,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"
@@ -520,7 +514,6 @@ REJECTED > APPROVED > DISLIKED > RECOMMENDED.
"mergeable": true, "mergeable": true,
"insertions": 126, "insertions": 126,
"deletions": 11, "deletions": 11,
"_sortkey": "0023412400000f7d",
"_number": 3965, "_number": 3965,
"owner": { "owner": {
"_account_id": 1000096, "_account_id": 1000096,
@@ -764,7 +757,6 @@ describes the abandoned change.
"mergeable": true, "mergeable": true,
"insertions": 3, "insertions": 3,
"deletions": 310, "deletions": 310,
"_sortkey": "0023412400000f7d",
"_number": 3965, "_number": 3965,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"
@@ -823,7 +815,6 @@ describes the restored change.
"mergeable": true, "mergeable": true,
"insertions": 2, "insertions": 2,
"deletions": 13, "deletions": 13,
"_sortkey": "0023412400000f7d",
"_number": 3965, "_number": 3965,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"
@@ -880,7 +871,6 @@ is included.
"mergeable": false, "mergeable": false,
"insertions": 33, "insertions": 33,
"deletions": 9, "deletions": 9,
"_sortkey": "0024cf9a000012bf",
"_number": 4799, "_number": 4799,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"
@@ -973,7 +963,6 @@ describes the reverting change.
"mergeable": true, "mergeable": true,
"insertions": 6, "insertions": 6,
"deletions": 4, "deletions": 4,
"_sortkey": "0023412400000f7d",
"_number": 3965, "_number": 3965,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"
@@ -1035,7 +1024,6 @@ describes the submitted/merged change.
"status": "MERGED", "status": "MERGED",
"created": "2013-02-01 09:59:32.126000000", "created": "2013-02-01 09:59:32.126000000",
"updated": "2013-02-21 11:16:36.775000000", "updated": "2013-02-21 11:16:36.775000000",
"_sortkey": "0023412400000f7d",
"_number": 3965, "_number": 3965,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"
@@ -1179,7 +1167,6 @@ missing from the result. At least `id`, `project`, `branch`, and
"mergeable": true, "mergeable": true,
"insertions": 34, "insertions": 34,
"deletions": 101, "deletions": 101,
"_sortkey": "0023412400000f7d",
"_number": 3965, "_number": 3965,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"
@@ -1228,7 +1215,6 @@ Only the change owner, a project owner, or an administrator may fix changes.
"mergeable": true, "mergeable": true,
"insertions": 34, "insertions": 34,
"deletions": 101, "deletions": 101,
"_sortkey": "0023412400000f7d",
"_number": 3965, "_number": 3965,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"
@@ -1863,7 +1849,6 @@ for the current patch set.
"mergeable": true, "mergeable": true,
"insertions": 34, "insertions": 34,
"deletions": 45, "deletions": 45,
"_sortkey": "0023412400000f7d",
"_number": 3965, "_number": 3965,
"owner": { "owner": {
"_account_id": 1000096, "_account_id": 1000096,
@@ -2128,7 +2113,6 @@ is included.
"mergeable": false, "mergeable": false,
"insertions": 21, "insertions": 21,
"deletions": 21, "deletions": 21,
"_sortkey": "0024cf9a000012bf",
"_number": 4799, "_number": 4799,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"
@@ -3096,7 +3080,6 @@ describes the resulting cherry picked change.
"mergeable": true, "mergeable": true,
"insertions": 12, "insertions": 12,
"deletions": 11, "deletions": 11,
"_sortkey": "0023412400000f7d",
"_number": 3965, "_number": 3965,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"
@@ -3150,7 +3133,6 @@ describes the change.
"mergeable": true, "mergeable": true,
"insertions": 261, "insertions": 261,
"deletions": 101, "deletions": 101,
"_sortkey": "0023412400000f7d",
"_number": 3965, "_number": 3965,
"owner": { "owner": {
"name": "John Doe" "name": "John Doe"
@@ -3335,7 +3317,6 @@ Not set for merged changes, or if the change has not yet been tested.
Number of inserted lines. Number of inserted lines.
|`deletions` || |`deletions` ||
Number of deleted lines. Number of deleted lines.
|`_sortkey` ||The sortkey of the change.
|`_number` ||The legacy numeric ID of the change. |`_number` ||The legacy numeric ID of the change.
|`owner` || |`owner` ||
The owner of the change as an link:rest-api-accounts.html#account-info[ The owner of the change as an link:rest-api-accounts.html#account-info[
@@ -3373,7 +3354,7 @@ Only set if link:#current-revision[the current revision] is requested
if link:#all-revisions[all revisions] are requested. if link:#all-revisions[all revisions] are requested.
|`_more_changes` |optional, not set if `false`| |`_more_changes` |optional, not set if `false`|
Whether the query would deliver more results if not limited. + Whether the query would deliver more results if not limited. +
Only set on either the last or the first change that is returned. Only set on the last change that is returned.
|`problems` |optional| |`problems` |optional|
A list of link:#problem-info[ProblemInfo] entities describing potential A list of link:#problem-info[ProblemInfo] entities describing potential
problems with this change. Only set if link:#check[CHECK] is set. problems with this change. Only set if link:#check[CHECK] is set.

View File

@@ -31,7 +31,6 @@ public class ChangeInfo {
protected String topic; protected String topic;
protected boolean starred; protected boolean starred;
protected Timestamp lastUpdatedOn; protected Timestamp lastUpdatedOn;
protected String sortKey;
protected PatchSet.Id patchSetId; protected PatchSet.Id patchSetId;
protected boolean latest; protected boolean latest;
@@ -52,7 +51,6 @@ public class ChangeInfo {
branch = c.getDest().getShortName(); branch = c.getDest().getShortName();
topic = c.getTopic(); topic = c.getTopic();
lastUpdatedOn = c.getLastUpdatedOn(); lastUpdatedOn = c.getLastUpdatedOn();
sortKey = c.getSortKey();
patchSetId = patchId; patchSetId = patchId;
latest = patchSetId == null || patchSetId.equals(c.currentPatchSetId()); latest = patchSetId == null || patchSetId.equals(c.currentPatchSetId());
} }
@@ -112,8 +110,4 @@ public class ChangeInfo {
public java.sql.Timestamp getLastUpdatedOn() { public java.sql.Timestamp getLastUpdatedOn() {
return lastUpdatedOn; return lastUpdatedOn;
} }
public String getSortKey() {
return sortKey;
}
} }

View File

@@ -36,7 +36,6 @@ public class ChangeInfo {
public Integer insertions; public Integer insertions;
public Integer deletions; public Integer deletions;
public String _sortkey;
public String baseChange; public String baseChange;
public int _number; public int _number;

View File

@@ -96,7 +96,6 @@ public class ChangeInfo extends JavaScriptObject {
private final native String updatedRaw() /*-{ return this.updated; }-*/; private final native String updatedRaw() /*-{ return this.updated; }-*/;
public final native boolean starred() /*-{ return this.starred ? true : false; }-*/; public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/; public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
public final native String _sortkey() /*-{ return this._sortkey; }-*/;
public final native NativeMap<LabelInfo> all_labels() /*-{ return this.labels; }-*/; public final native NativeMap<LabelInfo> all_labels() /*-{ return this.labels; }-*/;
public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/; public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
public final native String current_revision() /*-{ return this.current_revision; }-*/; public final native String current_revision() /*-{ return this.current_revision; }-*/;

View File

@@ -14,8 +14,8 @@
package com.google.gerrit.httpd.rpc.change; package com.google.gerrit.httpd.rpc.change;
import com.google.gerrit.server.query.change.QueryProcessor; import com.google.gerrit.server.query.change.OutputStreamQuery;
import com.google.gerrit.server.query.change.QueryProcessor.OutputFormat; import com.google.gerrit.server.query.change.OutputStreamQuery.OutputFormat;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
@@ -31,11 +31,11 @@ import javax.servlet.http.HttpServletResponse;
@Singleton @Singleton
public class DeprecatedChangeQueryServlet extends HttpServlet { public class DeprecatedChangeQueryServlet extends HttpServlet {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final Provider<QueryProcessor> processor; private final Provider<OutputStreamQuery> queryProvider;
@Inject @Inject
DeprecatedChangeQueryServlet(Provider<QueryProcessor> processor) { DeprecatedChangeQueryServlet(Provider<OutputStreamQuery> queryProvider) {
this.processor = processor; this.queryProvider = queryProvider;
} }
@Override @Override
@@ -44,7 +44,7 @@ public class DeprecatedChangeQueryServlet extends HttpServlet {
rsp.setContentType("text/json"); rsp.setContentType("text/json");
rsp.setCharacterEncoding("UTF-8"); rsp.setCharacterEncoding("UTF-8");
QueryProcessor p = processor.get(); OutputStreamQuery p = queryProvider.get();
OutputFormat format = OutputFormat.JSON; OutputFormat format = OutputFormat.JSON;
try { try {
format = OutputFormat.valueOf(get(req, "format", format.toString())); format = OutputFormat.valueOf(get(req, "format", format.toString()));

View File

@@ -433,9 +433,7 @@ public final class Change {
@Column(id = 5) @Column(id = 5)
protected Timestamp lastUpdatedOn; protected Timestamp lastUpdatedOn;
/** A {@link #lastUpdatedOn} ASC,{@link #changeId} ASC for sorting. */ // DELETED: id = 6 (sortkey)
@Column(id = 6, length = 16)
protected String sortKey;
@Column(id = 7, name = "owner_account_id") @Column(id = 7, name = "owner_account_id")
protected Account.Id owner; protected Account.Id owner;
@@ -489,7 +487,6 @@ public final class Change {
rowVersion = other.rowVersion; rowVersion = other.rowVersion;
createdOn = other.createdOn; createdOn = other.createdOn;
lastUpdatedOn = other.lastUpdatedOn; lastUpdatedOn = other.lastUpdatedOn;
sortKey = other.sortKey;
owner = other.owner; owner = other.owner;
dest = other.dest; dest = other.dest;
open = other.open; open = other.open;
@@ -534,14 +531,6 @@ public final class Change {
return rowVersion; return rowVersion;
} }
public String getSortKey() {
return sortKey;
}
public void setSortKey(final String newSortKey) {
sortKey = newSortKey;
}
public Account.Id getOwner() { public Account.Id getOwner() {
return owner; return owner;
} }

View File

@@ -55,11 +55,6 @@ public interface ChangeAccess extends Access<Change, Change.Id> {
@Query("WHERE open = true AND dest = ?") @Query("WHERE open = true AND dest = ?")
ResultSet<Change> byBranchOpenAll(Branch.NameKey p) throws OrmException; ResultSet<Change> byBranchOpenAll(Branch.NameKey p) throws OrmException;
@Query("WHERE open = true AND dest.projectName = ? AND sortKey < ?"
+ " ORDER BY sortKey DESC LIMIT ?")
ResultSet<Change> byProjectOpenNext(Project.NameKey p, String sortKey,
int limit) throws OrmException;
@Query @Query
ResultSet<Change> all() throws OrmException; ResultSet<Change> all() throws OrmException;
} }

View File

@@ -75,7 +75,7 @@ ON changes (status, dest_project_name, dest_branch_name, last_updated_on);
-- covers: byProjectOpenAll -- covers: byProjectOpenAll
CREATE INDEX changes_byProjectOpen CREATE INDEX changes_byProjectOpen
ON changes (open, dest_project_name, sort_key); ON changes (open, dest_project_name, last_updated_on);
-- covers: byProject -- covers: byProject
CREATE INDEX changes_byProject CREATE INDEX changes_byProject

View File

@@ -83,7 +83,7 @@ ON changes (status, dest_project_name, dest_branch_name, last_updated_on)
-- covers: byProjectOpenPrev, byProjectOpenNext -- covers: byProjectOpenPrev, byProjectOpenNext
CREATE INDEX changes_byProjectOpen CREATE INDEX changes_byProjectOpen
ON changes (open, dest_project_name, sort_key) ON changes (open, dest_project_name, last_updated_on);
# #
-- covers: byProject -- covers: byProject

View File

@@ -124,7 +124,7 @@ WHERE status = 's';
-- covers: byProjectOpenAll -- covers: byProjectOpenAll
CREATE INDEX changes_byProjectOpen CREATE INDEX changes_byProjectOpen
ON changes (dest_project_name, sort_key) ON changes (dest_project_name, last_updated_on)
WHERE open = 'Y'; WHERE open = 'Y';
-- covers: byProject -- covers: byProject

View File

@@ -15,15 +15,10 @@
package com.google.gerrit.server; package com.google.gerrit.server;
import static com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy.RECEIVE_COMMITS; import static com.google.gerrit.server.change.PatchSetInserter.ValidatePolicy.RECEIVE_COMMITS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import com.google.gerrit.common.TimeUtil; import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
@@ -85,15 +80,6 @@ import java.util.Map;
@Singleton @Singleton
public class ChangeUtil { public class ChangeUtil {
/**
* Epoch for sort key calculations, Tue Sep 30 2008 17:00:00.
* <p>
* We overrun approximately 4,083 years later, so ~6092.
*/
@VisibleForTesting
private static final long SORT_KEY_EPOCH_MINS =
MINUTES.convert(1222819200L, SECONDS);
private static final Object uuidLock = new Object(); private static final Object uuidLock = new Object();
private static final int SEED = 0x2418e6f9; private static final int SEED = 0x2418e6f9;
private static int uuidPrefix; private static int uuidPrefix;
@@ -150,7 +136,6 @@ public class ChangeUtil {
public static void updated(Change c) { public static void updated(Change c) {
c.setLastUpdatedOn(TimeUtil.nowTs()); c.setLastUpdatedOn(TimeUtil.nowTs());
computeSortKey(c);
} }
public static void insertAncestors(ReviewDb db, PatchSet.Id id, RevCommit src) public static void insertAncestors(ReviewDb db, PatchSet.Id id, RevCommit src)
@@ -166,29 +151,6 @@ public class ChangeUtil {
db.patchSetAncestors().insert(toInsert); db.patchSetAncestors().insert(toInsert);
} }
public static String sortKey(long lastUpdatedMs, int id) {
long lastUpdatedMins = MINUTES.convert(lastUpdatedMs, MILLISECONDS);
long minsSinceEpoch = lastUpdatedMins - SORT_KEY_EPOCH_MINS;
StringBuilder r = new StringBuilder(16);
r.setLength(16);
formatHexInt(r, 0, Ints.checkedCast(minsSinceEpoch));
formatHexInt(r, 8, id);
return r.toString();
}
public static long parseSortKey(String sortKey) {
if ("z".equals(sortKey)) {
return Long.MAX_VALUE;
}
return Long.parseLong(sortKey, 16);
}
public static void computeSortKey(Change c) {
long lastUpdatedMs = c.getLastUpdatedOn().getTime();
int id = c.getId().get();
c.setSortKey(sortKey(lastUpdatedMs, id));
}
public static PatchSet.Id nextPatchSetId(Map<String, Ref> allRefs, public static PatchSet.Id nextPatchSetId(Map<String, Ref> allRefs,
PatchSet.Id id) { PatchSet.Id id) {
PatchSet.Id next = nextPatchSetId(id); PatchSet.Id next = nextPatchSetId(id);
@@ -585,19 +547,4 @@ public class ChangeUtil {
public static PatchSet.Id nextPatchSetId(PatchSet.Id id) { public static PatchSet.Id nextPatchSetId(PatchSet.Id id) {
return new PatchSet.Id(id.getParentKey(), id.get() + 1); return new PatchSet.Id(id.getParentKey(), id.get() + 1);
} }
private static final char[] hexchar =
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', //
'a', 'b', 'c', 'd', 'e', 'f'};
private static void formatHexInt(final StringBuilder dst, final int p, int w) {
int o = p + 7;
while (o >= p && w != 0) {
dst.setCharAt(o--, hexchar[w & 0xf]);
w >>>= 4;
}
while (o >= p) {
dst.setCharAt(o--, '0');
}
}
} }

View File

@@ -136,7 +136,6 @@ public class ChangeInserter {
patchSet.setRevision(new RevId(commit.name())); patchSet.setRevision(new RevId(commit.name()));
patchSetInfo = patchSetInfoFactory.get(commit, patchSet.getId()); patchSetInfo = patchSetInfoFactory.get(commit, patchSet.getId());
change.setCurrentPatchSet(patchSetInfo); change.setCurrentPatchSet(patchSetInfo);
ChangeUtil.computeSortKey(change);
} }
public Change getChange() { public Change getChange() {

View File

@@ -104,6 +104,7 @@ import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.SubmitRuleEvaluator; import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeData.ChangedLines; import com.google.gerrit.server.query.change.ChangeData.ChangedLines;
import com.google.gerrit.server.query.change.QueryResult;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Provider; import com.google.inject.Provider;
@@ -252,10 +253,16 @@ public class ChangeJson {
return format(cd, Optional.of(rsrc.getPatchSet().getId())); return format(cd, Optional.of(rsrc.getPatchSet().getId()));
} }
public List<List<ChangeInfo>> formatList2(List<List<ChangeData>> in) public List<List<ChangeInfo>> formatQueryResults(List<QueryResult> in)
throws OrmException { throws OrmException {
accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS)); accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
Iterable<ChangeData> all = Iterables.concat(in); Iterable<ChangeData> all = FluentIterable.from(in)
.transformAndConcat(new Function<QueryResult, List<ChangeData>>() {
@Override
public List<ChangeData> apply(QueryResult in) {
return in.changes();
}
});
ChangeData.ensureChangeLoaded(all); ChangeData.ensureChangeLoaded(all);
if (has(ALL_REVISIONS)) { if (has(ALL_REVISIONS)) {
ChangeData.ensureAllPatchSetsLoaded(all); ChangeData.ensureAllPatchSetsLoaded(all);
@@ -270,8 +277,12 @@ public class ChangeJson {
List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size()); List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size());
Map<Change.Id, ChangeInfo> out = Maps.newHashMap(); Map<Change.Id, ChangeInfo> out = Maps.newHashMap();
for (List<ChangeData> changes : in) { for (QueryResult r : in) {
res.add(toChangeInfo(out, changes, reviewed)); List<ChangeInfo> infos = toChangeInfo(out, r.changes(), reviewed);
if (r.moreChanges()) {
infos.get(infos.size() - 1)._moreChanges = true;
}
res.add(infos);
} }
accountLoader.fill(); accountLoader.fill();
return res; return res;
@@ -368,7 +379,6 @@ public class ChangeJson {
out.created = in.getCreatedOn(); out.created = in.getCreatedOn();
out.updated = in.getLastUpdatedOn(); out.updated = in.getLastUpdatedOn();
out._number = in.getId().get(); out._number = in.getId().get();
out._sortkey = in.getSortKey();
out.starred = userProvider.get().getStarredChanges().contains(in.getId()) out.starred = userProvider.get().getStarredChanges().contains(in.getId())
? true ? true
: null; : null;

View File

@@ -47,7 +47,6 @@ import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ApprovalsUtil; import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil; import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.GerritPersonIdent; import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.ProjectUtil; import com.google.gerrit.server.ProjectUtil;
@@ -271,7 +270,6 @@ public class Submit implements RestModifyView<RevisionResource, SubmitInput>,
if (change.getStatus().isOpen()) { if (change.getStatus().isOpen()) {
change.setStatus(Change.Status.SUBMITTED); change.setStatus(Change.Status.SUBMITTED);
change.setLastUpdatedOn(timestamp); change.setLastUpdatedOn(timestamp);
ChangeUtil.computeSortKey(change);
return change; return change;
} }
return null; return null;

View File

@@ -31,7 +31,6 @@ public class ChangeAttribute {
public Long createdOn; public Long createdOn;
public Long lastUpdated; public Long lastUpdated;
public String sortKey;
public Boolean open; public Boolean open;
public Change.Status status; public Change.Status status;
public List<MessageAttribute> comments; public List<MessageAttribute> comments;

View File

@@ -18,5 +18,5 @@ public class QueryStatsAttribute {
public final String type = "stats"; public final String type = "stats";
public int rowCount; public int rowCount;
public long runTimeMilliseconds; public long runTimeMilliseconds;
public String resumeSortKey; public boolean moreChanges;
} }

View File

@@ -164,7 +164,6 @@ public class EventFactory {
public void extend(ChangeAttribute a, Change change) { public void extend(ChangeAttribute a, Change change) {
a.createdOn = change.getCreatedOn().getTime() / 1000L; a.createdOn = change.getCreatedOn().getTime() / 1000L;
a.lastUpdated = change.getLastUpdatedOn().getTime() / 1000L; a.lastUpdated = change.getLastUpdatedOn().getTime() / 1000L;
a.sortKey = change.getSortKey();
a.open = change.getStatus().isOpen(); a.open = change.getStatus().isOpen();
} }

View File

@@ -111,6 +111,7 @@ import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.ProjectState; import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.RefControl; import com.google.gerrit.server.project.RefControl;
import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.QueryProcessor;
import com.google.gerrit.server.ssh.SshInfo; import com.google.gerrit.server.ssh.SshInfo;
import com.google.gerrit.server.util.LabelVote; import com.google.gerrit.server.util.LabelVote;
import com.google.gerrit.server.util.MagicBranch; import com.google.gerrit.server.util.MagicBranch;
@@ -336,6 +337,7 @@ public class ReceiveCommits {
@Inject @Inject
ReceiveCommits(final ReviewDb db, ReceiveCommits(final ReviewDb db,
final Provider<QueryProcessor> queryProcessor,
final SchemaFactory<ReviewDb> schemaFactory, final SchemaFactory<ReviewDb> schemaFactory,
final ChangeData.Factory changeDataFactory, final ChangeData.Factory changeDataFactory,
final ChangeUpdate.Factory updateFactory, final ChangeUpdate.Factory updateFactory,
@@ -472,7 +474,7 @@ public class ReceiveCommits {
}); });
advHooks.add(rp.getAdvertiseRefsHook()); advHooks.add(rp.getAdvertiseRefsHook());
advHooks.add(new ReceiveCommitsAdvertiseRefsHook( advHooks.add(new ReceiveCommitsAdvertiseRefsHook(
db, projectControl.getProject().getNameKey())); db, queryProcessor, projectControl.getProject().getNameKey()));
rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks)); rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
} }

View File

@@ -18,13 +18,18 @@ import static org.eclipse.jgit.lib.RefDatabase.ALL;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.PatchSet; import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.QueryProcessor;
import com.google.gerrit.server.util.MagicBranch; import com.google.gerrit.server.util.MagicBranch;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.inject.Provider;
import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectId;
@@ -39,6 +44,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -48,11 +54,14 @@ public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
.getLogger(ReceiveCommitsAdvertiseRefsHook.class); .getLogger(ReceiveCommitsAdvertiseRefsHook.class);
private final ReviewDb db; private final ReviewDb db;
private final Provider<QueryProcessor> queryProcessor;
private final Project.NameKey projectName; private final Project.NameKey projectName;
public ReceiveCommitsAdvertiseRefsHook(ReviewDb db, public ReceiveCommitsAdvertiseRefsHook(ReviewDb db,
Provider<QueryProcessor> queryProcessor,
Project.NameKey projectName) { Project.NameKey projectName) {
this.db = db; this.db = db;
this.queryProcessor = queryProcessor;
this.projectName = projectName; this.projectName = projectName;
} }
@@ -93,10 +102,11 @@ public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
Set<ObjectId> toInclude = Sets.newHashSet(); Set<ObjectId> toInclude = Sets.newHashSet();
// Advertise some recent open changes, in case a commit is based one. // Advertise some recent open changes, in case a commit is based one.
final int limit = 32;
try { try {
Set<PatchSet.Id> toGet = Sets.newHashSetWithExpectedSize(32); Set<PatchSet.Id> toGet = Sets.newHashSetWithExpectedSize(limit);
for (Change c : db.changes().byProjectOpenNext(projectName, "z", 32)) { for (ChangeData cd : queryRecentChanges(limit)) {
PatchSet.Id id = c.currentPatchSetId(); PatchSet.Id id = cd.change().currentPatchSetId();
if (id != null) { if (id != null) {
toGet.add(id); toGet.add(id);
} }
@@ -164,6 +174,20 @@ public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
return toInclude; return toInclude;
} }
private List<ChangeData> queryRecentChanges(int limit)
throws OrmException {
QueryProcessor qp = queryProcessor.get();
qp.setLimit(limit);
ChangeQueryBuilder qb = qp.getQueryBuilder();
Predicate<ChangeData> p =
Predicate.and(qb.project(projectName.get()), qb.status_open());
try {
return qp.queryChanges(p).changes();
} catch (QueryParseException e) {
throw new OrmException(e);
}
}
private static boolean skip(String name) { private static boolean skip(String name) {
return name.startsWith(RefNames.REFS_CHANGES) return name.startsWith(RefNames.REFS_CHANGES)
|| name.startsWith(RefNames.REFS_CACHE_AUTOMERGE) || name.startsWith(RefNames.REFS_CACHE_AUTOMERGE)

View File

@@ -14,9 +14,8 @@
package com.google.gerrit.server.index; package com.google.gerrit.server.index;
import static com.google.gerrit.common.data.GlobalCapability.DEFAULT_MAX_QUERY_LIMIT; import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.base.MoreObjects;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
@@ -29,9 +28,9 @@ import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.AndSource; import com.google.gerrit.server.query.change.AndSource;
import com.google.gerrit.server.query.change.BasicChangeRewrites; import com.google.gerrit.server.query.change.BasicChangeRewrites;
import com.google.gerrit.server.query.change.ChangeData; import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeQueryRewriter; import com.google.gerrit.server.query.change.ChangeQueryRewriter;
import com.google.gerrit.server.query.change.ChangeStatusPredicate; import com.google.gerrit.server.query.change.ChangeStatusPredicate;
import com.google.gerrit.server.query.change.LimitPredicate;
import com.google.gerrit.server.query.change.OrSource; import com.google.gerrit.server.query.change.OrSource;
import com.google.inject.Inject; import com.google.inject.Inject;
@@ -129,16 +128,14 @@ public class IndexRewriteImpl implements ChangeQueryRewriter {
} }
@Override @Override
public Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start) public Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start,
throws QueryParseException { int limit) throws QueryParseException {
checkArgument(limit > 0, "limit must be positive: %s", limit);
ChangeIndex index = indexes.getSearchIndex(); ChangeIndex index = indexes.getSearchIndex();
in = basicRewrites.rewrite(in); in = basicRewrites.rewrite(in);
int limit = MoreObjects.firstNonNull(
ChangeQueryBuilder.getLimit(in), DEFAULT_MAX_QUERY_LIMIT);
// Increase the limit rather than skipping, since we don't know how many // Increase the limit rather than skipping, since we don't know how many
// skipped results would have been filtered out by the enclosing AndSource. // skipped results would have been filtered out by the enclosing AndSource.
limit += start; limit += start;
limit = Math.max(limit, 1);
Predicate<ChangeData> out = rewriteImpl(in, index, limit); Predicate<ChangeData> out = rewriteImpl(in, index, limit);
if (in == out || out instanceof IndexPredicate) { if (in == out || out instanceof IndexPredicate) {
@@ -168,6 +165,9 @@ public class IndexRewriteImpl implements ChangeQueryRewriter {
ChangeIndex index, int limit) throws QueryParseException { ChangeIndex index, int limit) throws QueryParseException {
if (isIndexPredicate(in, index)) { if (isIndexPredicate(in, index)) {
return in; return in;
} else if (in instanceof LimitPredicate) {
// Replace any limits with the limit provided by the caller.
return new LimitPredicate(limit);
} else if (!isRewritePossible(in)) { } else if (!isRewritePossible(in)) {
return null; // magic to indicate "in" cannot be rewritten return null; // magic to indicate "in" cannot be rewritten
} }

View File

@@ -138,7 +138,8 @@ public abstract class QueryBuilder<T> {
* @param p the predicate to find. * @param p the predicate to find.
* @param clazz type of the predicate instance. * @param clazz type of the predicate instance.
* @param name name of the operator. * @param name name of the operator.
* @return the predicate, null if not found. * @return the first instance of a predicate having the given type, as found
* by a depth-first search.
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static <T, P extends OperatorPredicate<T>> P find(Predicate<T> p, public static <T, P extends OperatorPredicate<T>> P find(Predicate<T> p,

View File

@@ -16,13 +16,11 @@ package com.google.gerrit.server.query.change;
import com.google.gerrit.reviewdb.client.Change; import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.query.IntPredicate;
import com.google.gerrit.server.query.Predicate; import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryRewriter; import com.google.gerrit.server.query.QueryRewriter;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.OutOfScopeException; import com.google.inject.OutOfScopeException;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.name.Named;
public class BasicChangeRewrites extends QueryRewriter<ChangeData> { public class BasicChangeRewrites extends QueryRewriter<ChangeData> {
private static final ChangeQueryBuilder BUILDER = new ChangeQueryBuilder( private static final ChangeQueryBuilder BUILDER = new ChangeQueryBuilder(
@@ -69,14 +67,6 @@ public class BasicChangeRewrites extends QueryRewriter<ChangeData> {
ChangeStatusPredicate.forStatus(Change.Status.MERGED)); ChangeStatusPredicate.forStatus(Change.Status.MERGED));
} }
@NoCostComputation
@Rewrite("A=(limit:*) B=(limit:*)")
public Predicate<ChangeData> r00_smallestLimit(
@Named("A") IntPredicate<ChangeData> a,
@Named("B") IntPredicate<ChangeData> b) {
return a.intValue() <= b.intValue() ? a : b;
}
private static final class InvalidProvider<T> implements Provider<T> { private static final class InvalidProvider<T> implements Provider<T> {
@Override @Override
public T get() { public T get() {

View File

@@ -45,7 +45,6 @@ import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.project.ChangeControl; import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.ListChildProjects; import com.google.gerrit.server.project.ListChildProjects;
import com.google.gerrit.server.project.ProjectCache; import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.IntPredicate;
import com.google.gerrit.server.query.Predicate; import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryBuilder; import com.google.gerrit.server.query.QueryBuilder;
import com.google.gerrit.server.query.QueryParseException; import com.google.gerrit.server.query.QueryParseException;
@@ -121,12 +120,6 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> mydef = private static final QueryBuilder.Definition<ChangeData, ChangeQueryBuilder> mydef =
new QueryBuilder.Definition<>(ChangeQueryBuilder.class); new QueryBuilder.Definition<>(ChangeQueryBuilder.class);
@SuppressWarnings("unchecked")
public static Integer getLimit(Predicate<ChangeData> p) {
IntPredicate<?> ip = find(p, IntPredicate.class, FIELD_LIMIT);
return ip != null ? ip.intValue() : null;
}
@VisibleForTesting @VisibleForTesting
public static class Arguments { public static class Arguments {
final Provider<ReviewDb> db; final Provider<ReviewDb> db;
@@ -633,28 +626,8 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
} }
@Operator @Operator
public Predicate<ChangeData> limit(String limit) { public Predicate<ChangeData> limit(String limit) throws QueryParseException {
return limit(Integer.parseInt(limit)); return new LimitPredicate(Integer.parseInt(limit));
}
static class LimitPredicate extends IntPredicate<ChangeData> {
LimitPredicate(int limit) {
super(FIELD_LIMIT, limit);
}
@Override
public boolean match(ChangeData object) {
return true;
}
@Override
public int getCost() {
return 0;
}
}
public Predicate<ChangeData> limit(int limit) {
return new LimitPredicate(limit);
} }
@Operator @Operator

View File

@@ -18,6 +18,6 @@ import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException; import com.google.gerrit.server.query.QueryParseException;
public interface ChangeQueryRewriter { public interface ChangeQueryRewriter {
Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start) Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int start, int limit)
throws QueryParseException; throws QueryParseException;
} }

View File

@@ -0,0 +1,47 @@
// Copyright (C) 2014 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.query.change.ChangeQueryBuilder.FIELD_LIMIT;
import com.google.gerrit.server.query.IntPredicate;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryBuilder;
import com.google.gerrit.server.query.QueryParseException;
public class LimitPredicate extends IntPredicate<ChangeData> {
@SuppressWarnings("unchecked")
public static Integer getLimit(Predicate<ChangeData> p) {
IntPredicate<?> ip = QueryBuilder.find(p, IntPredicate.class, FIELD_LIMIT);
return ip != null ? ip.intValue() : null;
}
public LimitPredicate(int limit) throws QueryParseException {
super(ChangeQueryBuilder.FIELD_LIMIT, limit);
if (limit <= 0) {
throw new QueryParseException("limit must be positive: " + limit);
}
}
@Override
public boolean match(ChangeData object) {
return true;
}
@Override
public int getCost() {
return 0;
}
}

View File

@@ -0,0 +1,405 @@
// Copyright (C) 2014 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.common.TimeUtil;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.data.ChangeAttribute;
import com.google.gerrit.server.data.PatchSetAttribute;
import com.google.gerrit.server.data.QueryStatsAttribute;
import com.google.gerrit.server.events.EventFactory;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gson.Gson;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
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.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* Change query implementation that outputs to a stream in the style of an SSH
* command.
*/
public class OutputStreamQuery {
private static final Logger log =
LoggerFactory.getLogger(OutputStreamQuery.class);
private static final DateTimeFormatter dtf =
DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss zzz");
public static enum OutputFormat {
TEXT, JSON
}
private final QueryProcessor queryProcessor;
private final EventFactory eventFactory;
private final TrackingFooters trackingFooters;
private final CurrentUser user;
private OutputFormat outputFormat = OutputFormat.TEXT;
private boolean includePatchSets;
private boolean includeCurrentPatchSet;
private boolean includeApprovals;
private boolean includeComments;
private boolean includeFiles;
private boolean includeCommitMessage;
private boolean includeDependencies;
private boolean includeSubmitRecords;
private boolean includeAllReviewers;
private OutputStream outputStream = DisabledOutputStream.INSTANCE;
private PrintWriter out;
@Inject
OutputStreamQuery(QueryProcessor queryProcessor,
EventFactory eventFactory,
TrackingFooters trackingFooters,
CurrentUser user) {
this.queryProcessor = queryProcessor;
this.eventFactory = eventFactory;
this.trackingFooters = trackingFooters;
this.user = user;
}
void setLimit(int n) {
queryProcessor.setLimit(n);
}
public void setStart(int n) {
queryProcessor.setStart(n);
}
public void setIncludePatchSets(boolean on) {
includePatchSets = on;
}
public boolean getIncludePatchSets() {
return includePatchSets;
}
public void setIncludeCurrentPatchSet(boolean on) {
includeCurrentPatchSet = on;
}
public boolean getIncludeCurrentPatchSet() {
return includeCurrentPatchSet;
}
public void setIncludeApprovals(boolean on) {
includeApprovals = on;
}
public void setIncludeComments(boolean on) {
includeComments = on;
}
public void setIncludeFiles(boolean on) {
includeFiles = on;
}
public boolean getIncludeFiles() {
return includeFiles;
}
public void setIncludeDependencies(boolean on) {
includeDependencies = on;
}
public boolean getIncludeDependencies() {
return includeDependencies;
}
public void setIncludeCommitMessage(boolean on) {
includeCommitMessage = on;
}
public void setIncludeSubmitRecords(boolean on) {
includeSubmitRecords = on;
}
public void setIncludeAllReviewers(boolean on) {
includeAllReviewers = 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 {
if (queryProcessor.isDisabled()) {
ErrorMessage m = new ErrorMessage();
m.message = "query disabled";
show(m);
return;
}
try {
final QueryStatsAttribute stats = new QueryStatsAttribute();
stats.runTimeMilliseconds = TimeUtil.nowMs();
QueryResult results = queryProcessor.queryByString(queryString);
ChangeAttribute c = null;
for (ChangeData d : results.changes()) {
ChangeControl cc = d.changeControl().forUser(user);
LabelTypes labelTypes = cc.getLabelTypes();
c = eventFactory.asChangeAttribute(d.change());
eventFactory.extend(c, d.change());
if (!trackingFooters.isEmpty()) {
eventFactory.addTrackingIds(c,
trackingFooters.extract(d.commitFooters()));
}
if (includeAllReviewers) {
eventFactory.addAllReviewers(c, d.notes());
}
if (includeSubmitRecords) {
eventFactory.addSubmitRecords(c, new SubmitRuleEvaluator(d)
.setAllowClosed(true)
.setAllowDraft(true)
.canSubmit());
}
if (includeCommitMessage) {
eventFactory.addCommitMessage(c, d.commitMessage());
}
if (includePatchSets) {
if (includeFiles) {
eventFactory.addPatchSets(c, d.patches(),
includeApprovals ? d.approvals().asMap() : null,
includeFiles, d.change(), labelTypes);
} else {
eventFactory.addPatchSets(c, d.patches(),
includeApprovals ? d.approvals().asMap() : null,
labelTypes);
}
}
if (includeCurrentPatchSet) {
PatchSet current = d.currentPatchSet();
if (current != null) {
c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
eventFactory.addApprovals(c.currentPatchSet,
d.currentApprovals(), labelTypes);
if (includeFiles) {
eventFactory.addPatchSetFileNames(c.currentPatchSet,
d.change(), d.currentPatchSet());
}
}
}
if (includeComments) {
eventFactory.addComments(c, d.messages());
if (includePatchSets) {
for (PatchSetAttribute attribute : c.patchSets) {
eventFactory.addPatchSetComments(attribute, d.publishedComments());
}
}
}
if (includeDependencies) {
eventFactory.addDependencies(c, d.change());
}
show(c);
}
stats.rowCount = results.changes().size();
stats.moreChanges = results.moreChanges();
stats.runTimeMilliseconds =
TimeUtil.nowMs() - 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);
} catch (NoSuchChangeException e) {
log.error("Missing change: " + e.getMessage(), e);
ErrorMessage m = new ErrorMessage();
m.message = "missing change " + e.getMessage();
show(m);
}
} finally {
try {
out.flush();
} finally {
out = null;
}
}
}
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(new 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;
}
showField(f.getName(), val, depth);
}
}
private String indent(int spaces) {
if (spaces == 0) {
return "";
} else {
return String.format("%" + spaces + "s", " ");
}
}
private void showField(String field, Object value, int depth) {
final int spacesDepthRatio = 2;
String indent = indent(depth * spacesDepthRatio);
out.print(indent);
out.print(field);
out.print(':');
if (value instanceof String && ((String) value).contains("\n")) {
out.print(' ');
// Idention for multi-line text is
// current depth indetion + length of field + length of ": "
indent = indent(indent.length() + field.length() + spacesDepthRatio);
out.print(((String) value).replaceAll("\n", "\n" + indent).trim());
out.print('\n');
} else if (value instanceof Long && isDateField(field)) {
out.print(' ');
out.print(dtf.print(((Long) value) * 1000L));
out.print('\n');
} else if (isPrimitive(value)) {
out.print(' ');
out.print(value);
out.print('\n');
} else if (value instanceof Collection) {
out.print('\n');
boolean firstElement = true;
for (Object thing : ((Collection<?>) value)) {
// The name of the collection was initially printed at the beginning
// of this routine. Beginning at the second sub-element, reprint
// the collection name so humans can separate individual elements
// with less strain and error.
//
if (firstElement) {
firstElement = false;
} else {
out.print(indent);
out.print(field);
out.print(":\n");
}
if (isPrimitive(thing)) {
out.print(' ');
out.print(value);
out.print('\n');
} else {
showText(thing, depth + 1);
}
}
} else {
out.print('\n');
showText(value, depth + 1);
}
}
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) //
|| "timestamp".equals(name) //
|| "createdOn".equals(name);
}
private List<Field> fieldsOf(Class<?> type) {
List<Field> r = new ArrayList<>();
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

@@ -31,7 +31,6 @@ import com.google.inject.Provider;
import org.kohsuke.args4j.Option; import org.kohsuke.args4j.Option;
import java.util.BitSet;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.List; import java.util.List;
@@ -42,7 +41,6 @@ public class QueryChanges implements RestReadView<TopLevelResource> {
private final ChangeJson json; private final ChangeJson json;
private final QueryProcessor imp; private final QueryProcessor imp;
private final Provider<CurrentUser> user; private final Provider<CurrentUser> user;
private boolean reverse;
private EnumSet<ListChangesOption> options; private EnumSet<ListChangesOption> options;
@Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", usage = "Query string") @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", usage = "Query string")
@@ -137,32 +135,15 @@ public class QueryChanges implements RestReadView<TopLevelResource> {
private List<List<ChangeInfo>> query0() throws OrmException, private List<List<ChangeInfo>> query0() throws OrmException,
QueryParseException { QueryParseException {
int cnt = queries.size(); int cnt = queries.size();
BitSet more = new BitSet(cnt); List<QueryResult> results = imp.queryByStrings(queries);
List<List<ChangeData>> data = imp.queryChanges(queries); List<List<ChangeInfo>> res = json.addOptions(options)
for (int n = 0; n < cnt; n++) { .formatQueryResults(results);
List<ChangeData> changes = data.get(n);
if (imp.getLimit() > 0 && changes.size() > imp.getLimit()) {
if (reverse) {
changes = changes.subList(1, changes.size());
} else {
changes = changes.subList(0, imp.getLimit());
}
data.set(n, changes);
more.set(n, true);
}
}
List<List<ChangeInfo>> res = json.addOptions(options).formatList2(data);
for (int n = 0; n < cnt; n++) { for (int n = 0; n < cnt; n++) {
List<ChangeInfo> info = res.get(n); List<ChangeInfo> info = res.get(n);
if (more.get(n) && !info.isEmpty()) { if (results.get(n).moreChanges()) {
if (reverse) {
info.get(0)._moreChanges = true;
} else {
info.get(info.size() - 1)._moreChanges = true; info.get(info.size() - 1)._moreChanges = true;
} }
} }
}
return res; return res;
} }
} }

View File

@@ -14,494 +14,178 @@
package com.google.gerrit.server.query.change; package com.google.gerrit.server.query.change;
import com.google.common.base.MoreObjects; import static com.google.common.base.Preconditions.checkState;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists; import com.google.common.collect.Ordering;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.data.GlobalCapability; import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.TrackingFooters; import com.google.gerrit.server.index.IndexPredicate;
import com.google.gerrit.server.data.ChangeAttribute;
import com.google.gerrit.server.data.PatchSetAttribute;
import com.google.gerrit.server.data.QueryStatsAttribute;
import com.google.gerrit.server.events.EventFactory;
import com.google.gerrit.server.project.ChangeControl;
import com.google.gerrit.server.project.NoSuchChangeException;
import com.google.gerrit.server.project.SubmitRuleEvaluator;
import com.google.gerrit.server.query.Predicate; import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException; import com.google.gerrit.server.query.QueryParseException;
import com.google.gson.Gson;
import com.google.gwtorm.server.OrmException; import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.ResultSet; import com.google.gwtorm.server.ResultSet;
import com.google.inject.Inject; import com.google.inject.Inject;
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.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List; import java.util.List;
public class QueryProcessor { 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 EventFactory eventFactory;
private final ChangeQueryBuilder queryBuilder; private final ChangeQueryBuilder queryBuilder;
private final ChangeQueryRewriter queryRewriter; private final ChangeQueryRewriter queryRewriter;
private final TrackingFooters trackingFooters; private final int permittedLimit;
private final CurrentUser user;
private final int maxLimit;
private OutputFormat outputFormat = OutputFormat.TEXT; private int limitFromCaller;
private int limit;
private int start; private int start;
private boolean includePatchSets;
private boolean includeCurrentPatchSet;
private boolean includeApprovals;
private boolean includeComments;
private boolean includeFiles;
private boolean includeCommitMessage;
private boolean includeDependencies;
private boolean includeSubmitRecords;
private boolean includeAllReviewers;
private OutputStream outputStream = DisabledOutputStream.INSTANCE;
private PrintWriter out;
private boolean moreResults;
@Inject @Inject
QueryProcessor(EventFactory eventFactory, QueryProcessor(ChangeQueryBuilder.Factory queryBuilder,
ChangeQueryBuilder.Factory queryBuilder, CurrentUser currentUser, CurrentUser currentUser,
ChangeQueryRewriter queryRewriter, ChangeQueryRewriter queryRewriter) {
TrackingFooters trackingFooters) {
this.eventFactory = eventFactory;
this.queryBuilder = queryBuilder.create(currentUser); this.queryBuilder = queryBuilder.create(currentUser);
this.queryRewriter = queryRewriter; this.queryRewriter = queryRewriter;
this.trackingFooters = trackingFooters; this.permittedLimit = currentUser.getCapabilities()
this.user = currentUser;
this.maxLimit = currentUser.getCapabilities()
.getRange(GlobalCapability.QUERY_LIMIT) .getRange(GlobalCapability.QUERY_LIMIT)
.getMax(); .getMax();
this.moreResults = false;
} }
int getLimit() { public ChangeQueryBuilder getQueryBuilder() {
return limit; return queryBuilder;
} }
void setLimit(int n) { public void setLimit(int n) {
limit = n; limitFromCaller = n;
} }
public void setStart(int n) { public void setStart(int n) {
start = n; start = n;
} }
public void setIncludePatchSets(boolean on) { /**
includePatchSets = on; * Query for changes that match the query string.
} *
* @see #queryChanges(List)
public boolean getIncludePatchSets() { * @param queryString the query string to parse.
return includePatchSets; * @return results of the query.
} */
public QueryResult queryByString(String queryString)
public void setIncludeCurrentPatchSet(boolean on) { throws OrmException, QueryParseException {
includeCurrentPatchSet = on; return queryByStrings(ImmutableList.of(queryString)).get(0);
}
public boolean getIncludeCurrentPatchSet() {
return includeCurrentPatchSet;
}
public void setIncludeApprovals(boolean on) {
includeApprovals = on;
}
public void setIncludeComments(boolean on) {
includeComments = on;
}
public void setIncludeFiles(boolean on) {
includeFiles = on;
}
public boolean getIncludeFiles() {
return includeFiles;
}
public void setIncludeDependencies(boolean on) {
includeDependencies = on;
}
public boolean getIncludeDependencies() {
return includeDependencies;
}
public void setIncludeCommitMessage(boolean on) {
includeCommitMessage = on;
}
public void setIncludeSubmitRecords(boolean on) {
includeSubmitRecords = on;
}
public void setIncludeAllReviewers(boolean on) {
includeAllReviewers = on;
}
public void setOutput(OutputStream out, OutputFormat fmt) {
this.outputStream = out;
this.outputFormat = fmt;
} }
/** /**
* Query for changes that match the query string. * Perform multiple queries over a list of query strings.
*
* @see #queryChanges(List)
* @param queryStrings the query strings to parse.
* @return results of the queries, one list per input query.
*/
public List<QueryResult> queryByStrings(List<String> queryStrings)
throws OrmException, QueryParseException {
List<Predicate<ChangeData>> queries = new ArrayList<>(queryStrings.size());
for (String qs : queryStrings) {
queries.add(queryBuilder.parse(qs));
}
return queryChanges(queries);
}
/**
* Query for changes that match a structured query.
*
* @see #queryChanges(List)
* @param query the query.
* @return results of the query.
*/
public QueryResult queryChanges(Predicate<ChangeData> query)
throws OrmException, QueryParseException {
return queryChanges(ImmutableList.of(query)).get(0);
}
/*
* Perform multiple queries over a list of query strings.
* <p> * <p>
* If a limit was specified using {@link #setLimit(int)} this method may * If a limit was specified using {@link #setLimit(int)} this method may
* return up to {@code limit + 1} results, allowing the caller to determine if * return up to {@code limit + 1} results, allowing the caller to determine if
* there are more than {@code limit} matches and suggest to its own caller * there are more than {@code limit} matches and suggest to its own caller
* that the query could be retried with {@link #setStart(int)}. * that the query could be retried with {@link #setStart(int)}.
*
* @param queries the queries.
* @return results of the queries, one list per input query.
*/ */
public List<ChangeData> queryChanges(String queryString) public List<QueryResult> queryChanges(List<Predicate<ChangeData>> queries)
throws OrmException, QueryParseException { throws OrmException, QueryParseException {
return queryChanges(ImmutableList.of(queryString)).get(0); return queryChanges(null, queries);
} }
/** static {
* Query for changes that match the query string. // In addition to this assumption, this queryChanges assumes the basic
* <p> // rewrites do not touch visibleto predicates either.
* If a limit was specified using {@link #setLimit(int)} this method may checkState(
* return up to {@code limit + 1} results, allowing the caller to determine if !IsVisibleToPredicate.class.isAssignableFrom(IndexPredicate.class),
* there are more than {@code limit} matches and suggest to its own caller "QueryProcessor assumes visibleto is not used by the index rewriter.");
* that the query could be retried with {@link #setStart(int)}. }
*/
public List<List<ChangeData>> queryChanges(List<String> queries) private List<QueryResult> queryChanges(List<String> queryStrings,
List<Predicate<ChangeData>> queries)
throws OrmException, QueryParseException { throws OrmException, QueryParseException {
final Predicate<ChangeData> visibleToMe = queryBuilder.is_visible(); Predicate<ChangeData> visibleToMe = queryBuilder.is_visible();
int cnt = queries.size(); int cnt = queries.size();
// Parse and rewrite all queries. // Parse and rewrite all queries.
List<Integer> limits = Lists.newArrayListWithCapacity(cnt); List<Integer> limits = new ArrayList<>(cnt);
List<ChangeDataSource> sources = Lists.newArrayListWithCapacity(cnt); List<Predicate<ChangeData>> predicates = new ArrayList<>(cnt);
for (String query : queries) { List<ChangeDataSource> sources = new ArrayList<>(cnt);
Predicate<ChangeData> q = parseQuery(query, visibleToMe); for (Predicate<ChangeData> q : queries) {
Predicate<ChangeData> s = queryRewriter.rewrite(q, start); int limit = getEffectiveLimit(q);
limits.add(limit);
// Always bump limit by 1, even if this results in exceeding the permitted
// max for this user. The only way to see if there are more changes is to
// ask for one more result from the query.
Predicate<ChangeData> s = queryRewriter.rewrite(q, start, limit + 1);
if (!(s instanceof ChangeDataSource)) { if (!(s instanceof ChangeDataSource)) {
q = Predicate.and(queryBuilder.status_open(), q); q = Predicate.and(queryBuilder.status_open(), q);
s = queryRewriter.rewrite(q, start); s = queryRewriter.rewrite(q, start, limit);
} }
if (!(s instanceof ChangeDataSource)) { if (!(s instanceof ChangeDataSource)) {
throw new QueryParseException("invalid query: " + s); throw new QueryParseException("invalid query: " + s);
} }
// Don't trust QueryRewriter to have left the visible predicate.
AndSource a = new AndSource(ImmutableList.of(s, visibleToMe), start); AndSource a = new AndSource(ImmutableList.of(s, visibleToMe), start);
limits.add(limit(q)); predicates.add(a);
sources.add(a); sources.add(a);
} }
// Run each query asynchronously, if supported. // Run each query asynchronously, if supported.
List<ResultSet<ChangeData>> matches = Lists.newArrayListWithCapacity(cnt); List<ResultSet<ChangeData>> matches = new ArrayList<>(cnt);
for (ChangeDataSource s : sources) { for (ChangeDataSource s : sources) {
matches.add(s.read()); matches.add(s.read());
} }
List<List<ChangeData>> out = Lists.newArrayListWithCapacity(cnt); List<QueryResult> out = new ArrayList<>(cnt);
for (int i = 0; i < cnt; i++) { for (int i = 0; i < cnt; i++) {
List<ChangeData> results = matches.get(i).toList(); out.add(QueryResult.create(
if (results.size() > maxLimit) { queryStrings != null ? queryStrings.get(i) : null,
moreResults = true; predicates.get(i),
} limits.get(i),
int limit = limits.get(i); matches.get(i).toList()));
if (limit < results.size()) {
results = results.subList(0, limit);
}
out.add(results);
} }
return out; return out;
} }
public void query(String queryString) throws IOException {
out = new PrintWriter( //
new BufferedWriter( //
new OutputStreamWriter(outputStream, "UTF-8")));
try {
if (isDisabled()) {
ErrorMessage m = new ErrorMessage();
m.message = "query disabled";
show(m);
return;
}
try {
final QueryStatsAttribute stats = new QueryStatsAttribute();
stats.runTimeMilliseconds = TimeUtil.nowMs();
List<ChangeData> results = queryChanges(queryString);
ChangeAttribute c = null;
for (ChangeData d : results) {
ChangeControl cc = d.changeControl().forUser(user);
LabelTypes labelTypes = cc.getLabelTypes();
c = eventFactory.asChangeAttribute(d.change());
eventFactory.extend(c, d.change());
if (!trackingFooters.isEmpty()) {
eventFactory.addTrackingIds(c,
trackingFooters.extract(d.commitFooters()));
}
if (includeAllReviewers) {
eventFactory.addAllReviewers(c, d.notes());
}
if (includeSubmitRecords) {
eventFactory.addSubmitRecords(c, new SubmitRuleEvaluator(d)
.setAllowClosed(true)
.setAllowDraft(true)
.canSubmit());
}
if (includeCommitMessage) {
eventFactory.addCommitMessage(c, d.commitMessage());
}
if (includePatchSets) {
if (includeFiles) {
eventFactory.addPatchSets(c, d.patches(),
includeApprovals ? d.approvals().asMap() : null,
includeFiles, d.change(), labelTypes);
} else {
eventFactory.addPatchSets(c, d.patches(),
includeApprovals ? d.approvals().asMap() : null,
labelTypes);
}
}
if (includeCurrentPatchSet) {
PatchSet current = d.currentPatchSet();
if (current != null) {
c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
eventFactory.addApprovals(c.currentPatchSet,
d.currentApprovals(), labelTypes);
if (includeFiles) {
eventFactory.addPatchSetFileNames(c.currentPatchSet,
d.change(), d.currentPatchSet());
}
}
}
if (includeComments) {
eventFactory.addComments(c, d.messages());
if (includePatchSets) {
for (PatchSetAttribute attribute : c.patchSets) {
eventFactory.addPatchSetComments(attribute, d.publishedComments());
}
}
}
if (includeDependencies) {
eventFactory.addDependencies(c, d.change());
}
show(c);
}
stats.rowCount = results.size();
if (moreResults) {
stats.resumeSortKey = c.sortKey;
}
stats.runTimeMilliseconds =
TimeUtil.nowMs() - 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);
} catch (NoSuchChangeException e) {
log.error("Missing change: " + e.getMessage(), e);
ErrorMessage m = new ErrorMessage();
m.message = "missing change " + e.getMessage();
show(m);
}
} finally {
try {
out.flush();
} finally {
out = null;
}
}
}
boolean isDisabled() { boolean isDisabled() {
return maxLimit <= 0; return permittedLimit <= 0;
} }
private int limit(Predicate<ChangeData> s) { private int getEffectiveLimit(Predicate<ChangeData> p) {
int n = MoreObjects.firstNonNull(ChangeQueryBuilder.getLimit(s), maxLimit); List<Integer> possibleLimits = new ArrayList<>(3);
return limit > 0 ? Math.min(n, limit) + 1 : n + 1; possibleLimits.add(permittedLimit);
if (limitFromCaller > 0) {
possibleLimits.add(limitFromCaller);
} }
Integer limitFromPredicate = LimitPredicate.getLimit(p);
private Predicate<ChangeData> parseQuery(String queryString, if (limitFromPredicate != null) {
final Predicate<ChangeData> visibleToMe) throws QueryParseException { possibleLimits.add(limitFromPredicate);
return Predicate.and(queryBuilder.parse(queryString),
queryBuilder.limit(limit > 0 ? Math.min(limit, maxLimit) + 1 : maxLimit),
visibleToMe);
} }
return Ordering.natural().min(possibleLimits);
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;
}
showField(f.getName(), val, depth);
}
}
private String indent(int spaces) {
if (spaces == 0) {
return "";
} else {
return String.format("%" + spaces + "s", " ");
}
}
private void showField(String field, Object value, int depth) {
final int spacesDepthRatio = 2;
String indent = indent(depth * spacesDepthRatio);
out.print(indent);
out.print(field);
out.print(':');
if (value instanceof String && ((String) value).contains("\n")) {
out.print(' ');
// Idention for multi-line text is
// current depth indetion + length of field + length of ": "
indent = indent(indent.length() + field.length() + spacesDepthRatio);
out.print(((String) value).replaceAll("\n", "\n" + indent).trim());
out.print('\n');
} else if (value instanceof Long && isDateField(field)) {
out.print(' ');
out.print(sdf.format(new Date(((Long) value) * 1000L)));
out.print('\n');
} else if (isPrimitive(value)) {
out.print(' ');
out.print(value);
out.print('\n');
} else if (value instanceof Collection) {
out.print('\n');
boolean firstElement = true;
for (Object thing : ((Collection<?>) value)) {
// The name of the collection was initially printed at the beginning
// of this routine. Beginning at the second sub-element, reprint
// the collection name so humans can separate individual elements
// with less strain and error.
//
if (firstElement) {
firstElement = false;
} else {
out.print(indent);
out.print(field);
out.print(":\n");
}
if (isPrimitive(thing)) {
out.print(' ');
out.print(value);
out.print('\n');
} else {
showText(thing, depth + 1);
}
}
} else {
out.print('\n');
showText(value, depth + 1);
}
}
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) //
|| "timestamp".equals(name) //
|| "createdOn".equals(name);
}
private List<Field> fieldsOf(Class<?> type) {
List<Field> r = new ArrayList<>();
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

@@ -0,0 +1,59 @@
// Copyright (C) 2014 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.auto.value.AutoValue;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.server.query.Predicate;
import java.util.List;
/** Results of a query over changes. */
@AutoValue
public abstract class QueryResult {
static QueryResult create(@Nullable String query,
Predicate<ChangeData> predicate, int limit, List<ChangeData> changes) {
boolean moreChanges;
if (changes.size() > limit) {
moreChanges = true;
changes = changes.subList(0, limit);
} else {
moreChanges = false;
}
return new AutoValue_QueryResult(query, predicate, changes, moreChanges);
}
/**
* @return the original query string, or null if the query was created
* programmatically.
*/
@Nullable public abstract String query();
/**
* @return the predicate after all rewriting and other modification by the
* query subsystem.
*/
public abstract Predicate<ChangeData> predicate();
/** @return the query results. */
public abstract List<ChangeData> changes();
/**
* @return whether the query could be retried with
* {@link QueryProcessor#setStart(int)} to produce more results. Never
* true if {@link #changes()} is empty.
*/
public abstract boolean moreChanges();
}

View File

@@ -32,7 +32,7 @@ import java.util.List;
/** A version of the database schema. */ /** A version of the database schema. */
public abstract class SchemaVersion { public abstract class SchemaVersion {
/** The current schema version. */ /** The current schema version. */
public static final Class<Schema_101> C = Schema_101.class; public static final Class<Schema_102> C = Schema_102.class;
public static class Module extends AbstractModule { public static class Module extends AbstractModule {
@Override @Override

View File

@@ -0,0 +1,51 @@
// Copyright (C) 2014 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.schema;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.schema.sql.DialectPostgreSQL;
import com.google.gwtorm.schema.sql.SqlDialect;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.sql.SQLException;
import java.sql.Statement;
public class Schema_102 extends SchemaVersion {
@Inject
Schema_102(Provider<Schema_100> prior) {
super(prior);
}
@Override
protected void migrateData(ReviewDb db, UpdateUI ui)
throws OrmException, SQLException {
JdbcSchema schema = (JdbcSchema) db;
SqlDialect dialect = schema.getDialect();
try (Statement stmt = schema.getConnection().createStatement()) {
stmt.executeUpdate("DROP INDEX changes_byProjectOpen");
if (dialect instanceof DialectPostgreSQL) {
stmt.executeUpdate("CREATE INDEX changes_byProjectOpen"
+ " ON changes (dest_project_name, last_updated_on)"
+ " WHERE open = 'Y'");
} else {
stmt.executeUpdate("CREATE INDEX changes_byProjectOpen"
+ " ON changes (open, dest_project_name, last_updated_on)");
}
}
}
}

View File

@@ -34,7 +34,6 @@ import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId; import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
import com.google.gerrit.reviewdb.client.PatchSetInfo; import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.server.ReviewDb; import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountManager; import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthRequest; import com.google.gerrit.server.account.AuthRequest;
@@ -112,7 +111,6 @@ public class LabelNormalizerTest {
new Change.Id(1), userId, new Change.Id(1), userId,
new Branch.NameKey(allProjects, "refs/heads/master"), new Branch.NameKey(allProjects, "refs/heads/master"),
TimeUtil.nowTs()); TimeUtil.nowTs());
ChangeUtil.computeSortKey(change);
PatchSetInfo ps = new PatchSetInfo(new PatchSet.Id(change.getId(), 1)); PatchSetInfo ps = new PatchSetInfo(new PatchSet.Id(change.getId(), 1));
ps.setSubject("Test change"); ps.setSubject("Test change");
change.setCurrentPatchSet(ps); change.setCurrentPatchSet(ps);

View File

@@ -97,7 +97,7 @@ public class IndexRewriteTest {
parse("-status:abandoned (status:open OR status:merged)"); parse("-status:abandoned (status:open OR status:merged)");
assertEquals( assertEquals(
query(parse("status:new OR status:submitted OR status:draft OR status:merged")), query(parse("status:new OR status:submitted OR status:draft OR status:merged")),
rewrite.rewrite(in, 0)); rewrite.rewrite(in, 0, DEFAULT_MAX_QUERY_LIMIT));
} }
@Test @Test
@@ -158,24 +158,26 @@ public class IndexRewriteTest {
} }
@Test @Test
public void testLimit() throws Exception { public void testLimitArgumentOverridesAllLimitPredicates() throws Exception {
Predicate<ChangeData> in = parse("file:a limit:3"); Predicate<ChangeData> in = parse("limit:1 file:a limit:3");
Predicate<ChangeData> out = rewrite(in); Predicate<ChangeData> out = rewrite(in, 5);
assertSame(AndSource.class, out.getClass()); assertSame(AndSource.class, out.getClass());
assertEquals(ImmutableList.of( assertEquals(ImmutableList.of(
query(in.getChild(0), 3), query(in.getChild(1), 5),
in.getChild(1)), parse("limit:5"),
parse("limit:5")),
out.getChildren()); out.getChildren());
} }
@Test @Test
public void testStartIncreasesLimit() throws Exception { public void testStartIncreasesLimit() throws Exception {
int n = 3;
Predicate<ChangeData> f = parse("file:a"); Predicate<ChangeData> f = parse("file:a");
Predicate<ChangeData> l = parse("limit:3"); Predicate<ChangeData> l = parse("limit:" + n);
Predicate<ChangeData> in = and(f, l); Predicate<ChangeData> in = and(f, l);
assertEquals(and(query(f, 3), l), rewrite.rewrite(in, 0)); assertEquals(and(query(f, 3), parse("limit:3")), rewrite.rewrite(in, 0, n));
assertEquals(and(query(f, 4), l), rewrite.rewrite(in, 1)); assertEquals(and(query(f, 4), parse("limit:4")), rewrite.rewrite(in, 1, n));
assertEquals(and(query(f, 5), l), rewrite.rewrite(in, 2)); assertEquals(and(query(f, 5), parse("limit:5")), rewrite.rewrite(in, 2, n));
} }
@Test @Test
@@ -220,7 +222,12 @@ public class IndexRewriteTest {
private Predicate<ChangeData> rewrite(Predicate<ChangeData> in) private Predicate<ChangeData> rewrite(Predicate<ChangeData> in)
throws QueryParseException { throws QueryParseException {
return rewrite.rewrite(in, 0); return rewrite.rewrite(in, 0, DEFAULT_MAX_QUERY_LIMIT);
}
private Predicate<ChangeData> rewrite(Predicate<ChangeData> in, int limit)
throws QueryParseException {
return rewrite.rewrite(in, 0, limit);
} }
private IndexedChangeQuery query(Predicate<ChangeData> p) private IndexedChangeQuery query(Predicate<ChangeData> p)

View File

@@ -136,18 +136,22 @@ public abstract class AbstractQueryChangesTest {
userAccount.setPreferredEmail("user@example.com"); userAccount.setPreferredEmail("user@example.com");
db.accounts().update(ImmutableList.of(userAccount)); db.accounts().update(ImmutableList.of(userAccount));
user = userFactory.create(userId); user = userFactory.create(userId);
requestContext.setContext(newRequestContext(userAccount.getId()));
}
requestContext.setContext(new RequestContext() { private RequestContext newRequestContext(Account.Id requestUserId) {
final CurrentUser requestUser = userFactory.create(requestUserId);
return new RequestContext() {
@Override @Override
public CurrentUser getCurrentUser() { public CurrentUser getCurrentUser() {
return user; return requestUser;
} }
@Override @Override
public Provider<ReviewDb> getReviewDbProvider() { public Provider<ReviewDb> getReviewDbProvider() {
return Providers.of(db); return Providers.of(db);
} }
}); };
} }
@After @After
@@ -543,9 +547,22 @@ public abstract class AbstractQueryChangesTest {
List<ChangeInfo> results; List<ChangeInfo> results;
for (int i = 1; i <= n + 2; i++) { for (int i = 1; i <= n + 2; i++) {
int expectedSize;
Boolean expectedMoreChanges;
if (i < n) {
expectedSize = i;
expectedMoreChanges = true;
} else {
expectedSize = n;
expectedMoreChanges = null;
}
results = query("status:new limit:" + i); results = query("status:new limit:" + i);
assertThat(results).hasSize(Math.min(i, n)); String msg = "i=" + i;
assert_().withFailureMessage(msg).that(results).hasSize(expectedSize);
assertResultEquals(last, results.get(0)); assertResultEquals(last, results.get(0));
assert_().withFailureMessage(msg)
.that(results.get(results.size() - 1)._moreChanges)
.isEqualTo(expectedMoreChanges);
} }
} }
@@ -1031,6 +1048,49 @@ public abstract class AbstractQueryChangesTest {
assertThat(query("repo")).hasSize(6); assertThat(query("repo")).hasSize(6);
} }
@Test
public void implicitVisibleTo() throws Exception {
TestRepository<InMemoryRepository> repo = createProject("repo");
Change change1 = newChange(repo, null, null, userId.get(), null).insert();
ChangeInserter ins2 = newChange(repo, null, null, userId.get(), null);
Change change2 = ins2.getChange();
change2.setStatus(Change.Status.DRAFT);
ins2.insert();
String q = "project:repo";
List<ChangeInfo> results = query(q);
assertThat(results).hasSize(2);
assertResultEquals(change2, results.get(0));
assertResultEquals(change1, results.get(1));
// Second user cannot see first user's drafts.
requestContext.setContext(newRequestContext(accountManager
.authenticate(AuthRequest.forUser("anotheruser")).getAccountId()));
assertResultEquals(change1, queryOne(q));
}
@Test
public void explicitVisibleTo() throws Exception {
TestRepository<InMemoryRepository> repo = createProject("repo");
Change change1 = newChange(repo, null, null, userId.get(), null).insert();
ChangeInserter ins2 = newChange(repo, null, null, userId.get(), null);
Change change2 = ins2.getChange();
change2.setStatus(Change.Status.DRAFT);
ins2.insert();
String q = "project:repo";
List<ChangeInfo> results = query(q);
assertThat(results).hasSize(2);
assertResultEquals(change2, results.get(0));
assertResultEquals(change1, results.get(1));
// Second user cannot see first user's drafts.
Account.Id user2 = accountManager
.authenticate(AuthRequest.forUser("anotheruser"))
.getAccountId();
assertResultEquals(change1, queryOne(q + " visibleto:" + user2.get()));
}
protected ChangeInserter newChange( protected ChangeInserter newChange(
TestRepository<InMemoryRepository> repo, TestRepository<InMemoryRepository> repo,
@Nullable RevCommit commit, @Nullable String key, @Nullable Integer owner, @Nullable RevCommit commit, @Nullable String key, @Nullable Integer owner,

View File

@@ -25,7 +25,6 @@ import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetInfo; import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RevId; import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AllUsersName; import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.AllUsersNameProvider; import com.google.gerrit.server.config.AllUsersNameProvider;
@@ -114,6 +113,5 @@ public class TestChanges {
change.getId(), curr != null ? curr.get() + 1 : 1)); change.getId(), curr != null ? curr.get() + 1 : 1));
ps.setSubject("Change subject"); ps.setSubject("Change subject");
change.setCurrentPatchSet(ps); change.setCurrentPatchSet(ps);
ChangeUtil.computeSortKey(change);
} }
} }

View File

@@ -14,7 +14,8 @@
package com.google.gerrit.sshd.commands; package com.google.gerrit.sshd.commands;
import com.google.gerrit.server.query.change.QueryProcessor; import com.google.gerrit.server.query.change.OutputStreamQuery;
import com.google.gerrit.server.query.change.OutputStreamQuery.OutputFormat;
import com.google.gerrit.sshd.CommandMetaData; import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand; import com.google.gerrit.sshd.SshCommand;
import com.google.inject.Inject; import com.google.inject.Inject;
@@ -27,10 +28,10 @@ import java.util.List;
@CommandMetaData(name = "query", description = "Query the change database") @CommandMetaData(name = "query", description = "Query the change database")
class Query extends SshCommand { class Query extends SshCommand {
@Inject @Inject
private QueryProcessor processor; private OutputStreamQuery processor;
@Option(name = "--format", metaVar = "FMT", usage = "Output display format") @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
void setFormat(QueryProcessor.OutputFormat format) { void setFormat(OutputFormat format) {
processor.setOutput(out, format); processor.setOutput(out, format);
} }
@@ -97,7 +98,7 @@ class Query extends SshCommand {
@Override @Override
protected void parseCommandLine() throws UnloggedFailure { protected void parseCommandLine() throws UnloggedFailure {
processor.setOutput(out, QueryProcessor.OutputFormat.TEXT); processor.setOutput(out, OutputFormat.TEXT);
super.parseCommandLine(); super.parseCommandLine();
if (processor.getIncludeFiles() && if (processor.getIncludeFiles() &&
!(processor.getIncludePatchSets() || processor.getIncludeCurrentPatchSet())) { !(processor.getIncludePatchSets() || processor.getIncludeCurrentPatchSet())) {