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:
Shawn O. Pearce
2010-07-19 10:30:37 -07:00
parent a39a7d922d
commit 51e058f626
7 changed files with 145 additions and 9 deletions

View File

@@ -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'::
+

View File

@@ -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 =

View File

@@ -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() {
}
}

View File

@@ -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,

View File

@@ -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;
}
}

View File

@@ -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()) {

View File

@@ -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,