Add age: operator to change queries
The age operator looks for changes that have not been modified within the argument. E.g. "age:3d" will find changes that haven't been touched in the last 3 days. This supports searching for stale changes that maybe need to be considered for further action. Change-Id: I35c843099aac7bb795613c5898e84ef0a60f91cc Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
@@ -48,6 +48,21 @@ Operators act as restrictions on the search. As more operators
|
||||
are added to the same query string, they further restrict the
|
||||
returned results.
|
||||
|
||||
[[age]]
|
||||
age:'AGE'::
|
||||
+
|
||||
Amount of time that has expired since the change was last updated
|
||||
with a review comment or new patch set. The age must be specified
|
||||
to include a unit suffix, for example `age:2d`:
|
||||
+
|
||||
* s, sec, second, seconds
|
||||
* m, min, minute, minutes
|
||||
* h, hr, hour, hours
|
||||
* d, day, days
|
||||
* w, week, weeks (`1 week` is treated as `7 days`)
|
||||
* mon, month, months (`1 month` is treated as `30 days`)
|
||||
* y, year, years (`1 year` is treated as `365 days`)
|
||||
|
||||
[[change]]
|
||||
change:'ID'::
|
||||
+
|
||||
|
||||
@@ -177,18 +177,22 @@ public class ChangeUtil {
|
||||
return new PatchSetApproval(akey, (short) 1);
|
||||
}
|
||||
|
||||
|
||||
public static void computeSortKey(final Change c) {
|
||||
public static String sortKey(long lastUpdated, int id){
|
||||
// The encoding uses minutes since Wed Oct 1 00:00:00 2008 UTC.
|
||||
// We overrun approximately 4,085 years later, so ~6093.
|
||||
//
|
||||
final long lastUpdatedOn =
|
||||
(c.getLastUpdatedOn().getTime() / 1000L) - 1222819200L;
|
||||
final long lastUpdatedOn = (lastUpdated / 1000L) - 1222819200L;
|
||||
final StringBuilder r = new StringBuilder(16);
|
||||
r.setLength(16);
|
||||
formatHexInt(r, 0, (int) (lastUpdatedOn / 60));
|
||||
formatHexInt(r, 8, c.getId().get());
|
||||
c.setSortKey(r.toString());
|
||||
formatHexInt(r, 8, id);
|
||||
return r.toString();
|
||||
}
|
||||
|
||||
public static void computeSortKey(final Change c) {
|
||||
long lastUpdated = c.getLastUpdatedOn().getTime();
|
||||
int id = c.getId().get();
|
||||
c.setSortKey(sortKey(lastUpdated, id));
|
||||
}
|
||||
|
||||
private static final char[] hexchar =
|
||||
|
||||
@@ -140,6 +140,27 @@ public class ConfigUtil {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try {
|
||||
return getTimeUnit(s, defaultValue, wantUnit);
|
||||
} catch (IllegalArgumentException notTime) {
|
||||
throw notTimeUnit(section, subsection, setting, valueString);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a numerical time unit, such as "1 minute", from a string.
|
||||
*
|
||||
* @param s the string to parse.
|
||||
* @param defaultValue default value to return if no value was set in the
|
||||
* configuration file.
|
||||
* @param wantUnit the units of {@code defaultValue} and the return value, as
|
||||
* well as the units to assume if the value does not contain an
|
||||
* indication of the units.
|
||||
* @return the setting, or {@code defaultValue} if not set, expressed in
|
||||
* {@code units}.
|
||||
*/
|
||||
public static long getTimeUnit(String s, long defaultValue, TimeUnit wantUnit) {
|
||||
final String valueString = s;
|
||||
final String unitName;
|
||||
final int sp = s.indexOf(' ');
|
||||
if (sp > 0) {
|
||||
@@ -198,13 +219,13 @@ public class ConfigUtil {
|
||||
inputMul = 365;
|
||||
|
||||
} else {
|
||||
throw notTimeUnit(section, subsection, setting, valueString);
|
||||
throw notTimeUnit(valueString);
|
||||
}
|
||||
|
||||
try {
|
||||
return wantUnit.convert(Long.parseLong(s) * inputMul, inputUnit);
|
||||
} catch (NumberFormatException nfe) {
|
||||
throw notTimeUnit(section, subsection, setting, valueString);
|
||||
throw notTimeUnit(valueString);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +299,10 @@ public class ConfigUtil {
|
||||
+ valueString);
|
||||
}
|
||||
|
||||
private static IllegalArgumentException notTimeUnit(final String val) {
|
||||
return new IllegalArgumentException("Invalid time unit value: " + val);
|
||||
}
|
||||
|
||||
private ConfigUtil() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ public abstract class QueryRewriter<T> {
|
||||
old = in;
|
||||
in = rewriteOne(in);
|
||||
|
||||
if (in.getChildCount() > 0) {
|
||||
if (old.equals(in) && in.getChildCount() > 0) {
|
||||
List<Predicate<T>> n = new ArrayList<Predicate<T>>(in.getChildCount());
|
||||
for (Predicate<T> p : in.getChildren()) {
|
||||
n.add(rewrite(p));
|
||||
@@ -180,6 +180,10 @@ public abstract class QueryRewriter<T> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!r.useBestCost()) {
|
||||
return n;
|
||||
}
|
||||
|
||||
if (best == null || n.getCost() < best.getCost()) {
|
||||
best = n;
|
||||
continue;
|
||||
@@ -322,6 +326,11 @@ public abstract class QueryRewriter<T> {
|
||||
String value();
|
||||
}
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
protected @interface NoCostComputation {
|
||||
}
|
||||
|
||||
/** Applies a rewrite rule to a Predicate. */
|
||||
protected interface RewriteRule<T> {
|
||||
/**
|
||||
@@ -333,6 +342,9 @@ public abstract class QueryRewriter<T> {
|
||||
* rule does not want this predicate.
|
||||
*/
|
||||
Predicate<T> rewrite(QueryRewriter<T> rewriter, Predicate<T> input);
|
||||
|
||||
/** @return true if the best cost should be selected. */
|
||||
boolean useBestCost();
|
||||
}
|
||||
|
||||
/** Implements the magic behind {@link Rewrite} annotations. */
|
||||
@@ -341,10 +353,12 @@ public abstract class QueryRewriter<T> {
|
||||
private final Predicate<T> pattern;
|
||||
private final String[] argNames;
|
||||
private final Class<? extends Predicate<T>>[] argTypes;
|
||||
private final boolean useBestCost;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
MethodRewrite(QueryBuilder<T> queryBuilder, String patternText, Method m) {
|
||||
method = m;
|
||||
useBestCost = m.getAnnotation(NoCostComputation.class) == null;
|
||||
|
||||
Predicate<T> p;
|
||||
try {
|
||||
@@ -385,6 +399,11 @@ public abstract class QueryRewriter<T> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useBestCost() {
|
||||
return useBestCost;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Predicate<T> rewrite(QueryRewriter<T> rewriter,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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 static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
|
||||
import com.google.gerrit.reviewdb.Change;
|
||||
import com.google.gerrit.reviewdb.ReviewDb;
|
||||
import com.google.gerrit.server.config.ConfigUtil;
|
||||
import com.google.gerrit.server.query.OperatorPredicate;
|
||||
import com.google.gwtorm.client.OrmException;
|
||||
import com.google.inject.Provider;
|
||||
|
||||
class AgePredicate extends OperatorPredicate<ChangeData> {
|
||||
private final Provider<ReviewDb> dbProvider;
|
||||
private final long cut;
|
||||
|
||||
AgePredicate(Provider<ReviewDb> dbProvider, String value) {
|
||||
super(ChangeQueryBuilder.FIELD_AGE, value);
|
||||
this.dbProvider = dbProvider;
|
||||
|
||||
long s = ConfigUtil.getTimeUnit(getValue(), 0, SECONDS);
|
||||
long ms = MILLISECONDS.convert(s, SECONDS);
|
||||
this.cut = (System.currentTimeMillis() - ms) + 1;
|
||||
}
|
||||
|
||||
long getCut() {
|
||||
return cut;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(final ChangeData object) throws OrmException {
|
||||
Change change = object.change(dbProvider);
|
||||
return change != null && change.getLastUpdatedOn().getTime() < cut;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCost() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
|
||||
private static final Pattern PAT_LABEL =
|
||||
Pattern.compile("^[a-zA-Z][a-zA-Z0-9]*((=|>=|<=)[+-]?|[+-])\\d+$");
|
||||
|
||||
public static final String FIELD_AGE = "age";
|
||||
public static final String FIELD_BRANCH = "branch";
|
||||
public static final String FIELD_CHANGE = "change";
|
||||
public static final String FIELD_COMMIT = "commit";
|
||||
@@ -118,6 +119,11 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
|
||||
return dbProvider;
|
||||
}
|
||||
|
||||
@Operator
|
||||
public Predicate<ChangeData> age(String value) {
|
||||
return new AgePredicate(dbProvider, value);
|
||||
}
|
||||
|
||||
@Operator
|
||||
public Predicate<ChangeData> change(String query) {
|
||||
if (PAT_LEGACY_ID.matcher(query).matches()) {
|
||||
|
||||
@@ -17,6 +17,7 @@ package com.google.gerrit.server.query.change;
|
||||
import com.google.gerrit.reviewdb.Change;
|
||||
import com.google.gerrit.reviewdb.ChangeAccess;
|
||||
import com.google.gerrit.reviewdb.ReviewDb;
|
||||
import com.google.gerrit.server.ChangeUtil;
|
||||
import com.google.gerrit.server.CurrentUser;
|
||||
import com.google.gerrit.server.query.IntPredicate;
|
||||
import com.google.gerrit.server.query.Predicate;
|
||||
@@ -60,16 +61,19 @@ public class ChangeQueryRewriter extends QueryRewriter<ChangeData> {
|
||||
}
|
||||
|
||||
@Rewrite("-status:open")
|
||||
@NoCostComputation
|
||||
public Predicate<ChangeData> r00_notOpen() {
|
||||
return ChangeStatusPredicate.closed(dbProvider);
|
||||
}
|
||||
|
||||
@Rewrite("-status:closed")
|
||||
@NoCostComputation
|
||||
public Predicate<ChangeData> r00_notClosed() {
|
||||
return ChangeStatusPredicate.open(dbProvider);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@NoCostComputation
|
||||
@Rewrite("-status:merged")
|
||||
public Predicate<ChangeData> r00_notMerged() {
|
||||
return or(ChangeStatusPredicate.open(dbProvider),
|
||||
@@ -77,12 +81,21 @@ public class ChangeQueryRewriter extends QueryRewriter<ChangeData> {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@NoCostComputation
|
||||
@Rewrite("-status:abandoned")
|
||||
public Predicate<ChangeData> r00_notAbandoned() {
|
||||
return or(ChangeStatusPredicate.open(dbProvider),
|
||||
new ChangeStatusPredicate(dbProvider, Change.Status.MERGED));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@NoCostComputation
|
||||
@Rewrite("sortkey_before:z A=(age:*)")
|
||||
public Predicate<ChangeData> r00_ageToSortKey(@Named("A") AgePredicate a) {
|
||||
String cut = ChangeUtil.sortKey(a.getCut(), Integer.MAX_VALUE);
|
||||
return and(new SortKeyPredicate.Before(dbProvider, cut), a);
|
||||
}
|
||||
|
||||
@Rewrite("status:open P=(project:*) S=(sortkey_after:*) L=(limit:*)")
|
||||
public Predicate<ChangeData> r10_byProjectOpenPrev(
|
||||
@Named("P") final ProjectPredicate p,
|
||||
|
||||
Reference in New Issue
Block a user