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 are added to the same query string, they further restrict the
returned results. 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]]
change:'ID':: change:'ID'::
+ +

View File

@@ -177,18 +177,22 @@ public class ChangeUtil {
return new PatchSetApproval(akey, (short) 1); return new PatchSetApproval(akey, (short) 1);
} }
public static String sortKey(long lastUpdated, int id){
public static void computeSortKey(final Change c) {
// The encoding uses minutes since Wed Oct 1 00:00:00 2008 UTC. // The encoding uses minutes since Wed Oct 1 00:00:00 2008 UTC.
// We overrun approximately 4,085 years later, so ~6093. // We overrun approximately 4,085 years later, so ~6093.
// //
final long lastUpdatedOn = final long lastUpdatedOn = (lastUpdated / 1000L) - 1222819200L;
(c.getLastUpdatedOn().getTime() / 1000L) - 1222819200L;
final StringBuilder r = new StringBuilder(16); final StringBuilder r = new StringBuilder(16);
r.setLength(16); r.setLength(16);
formatHexInt(r, 0, (int) (lastUpdatedOn / 60)); formatHexInt(r, 0, (int) (lastUpdatedOn / 60));
formatHexInt(r, 8, c.getId().get()); formatHexInt(r, 8, id);
c.setSortKey(r.toString()); 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 = private static final char[] hexchar =

View File

@@ -140,6 +140,27 @@ public class ConfigUtil {
return defaultValue; 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 String unitName;
final int sp = s.indexOf(' '); final int sp = s.indexOf(' ');
if (sp > 0) { if (sp > 0) {
@@ -198,13 +219,13 @@ public class ConfigUtil {
inputMul = 365; inputMul = 365;
} else { } else {
throw notTimeUnit(section, subsection, setting, valueString); throw notTimeUnit(valueString);
} }
try { try {
return wantUnit.convert(Long.parseLong(s) * inputMul, inputUnit); return wantUnit.convert(Long.parseLong(s) * inputMul, inputUnit);
} catch (NumberFormatException nfe) { } catch (NumberFormatException nfe) {
throw notTimeUnit(section, subsection, setting, valueString); throw notTimeUnit(valueString);
} }
} }
@@ -278,6 +299,10 @@ public class ConfigUtil {
+ valueString); + valueString);
} }
private static IllegalArgumentException notTimeUnit(final String val) {
return new IllegalArgumentException("Invalid time unit value: " + val);
}
private ConfigUtil() { private ConfigUtil() {
} }
} }

View File

@@ -132,7 +132,7 @@ public abstract class QueryRewriter<T> {
old = in; old = in;
in = rewriteOne(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()); List<Predicate<T>> n = new ArrayList<Predicate<T>>(in.getChildCount());
for (Predicate<T> p : in.getChildren()) { for (Predicate<T> p : in.getChildren()) {
n.add(rewrite(p)); n.add(rewrite(p));
@@ -180,6 +180,10 @@ public abstract class QueryRewriter<T> {
continue; continue;
} }
if (!r.useBestCost()) {
return n;
}
if (best == null || n.getCost() < best.getCost()) { if (best == null || n.getCost() < best.getCost()) {
best = n; best = n;
continue; continue;
@@ -322,6 +326,11 @@ public abstract class QueryRewriter<T> {
String value(); String value();
} }
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
protected @interface NoCostComputation {
}
/** Applies a rewrite rule to a Predicate. */ /** Applies a rewrite rule to a Predicate. */
protected interface RewriteRule<T> { protected interface RewriteRule<T> {
/** /**
@@ -333,6 +342,9 @@ public abstract class QueryRewriter<T> {
* rule does not want this predicate. * rule does not want this predicate.
*/ */
Predicate<T> rewrite(QueryRewriter<T> rewriter, Predicate<T> input); 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. */ /** Implements the magic behind {@link Rewrite} annotations. */
@@ -341,10 +353,12 @@ public abstract class QueryRewriter<T> {
private final Predicate<T> pattern; private final Predicate<T> pattern;
private final String[] argNames; private final String[] argNames;
private final Class<? extends Predicate<T>>[] argTypes; private final Class<? extends Predicate<T>>[] argTypes;
private final boolean useBestCost;
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
MethodRewrite(QueryBuilder<T> queryBuilder, String patternText, Method m) { MethodRewrite(QueryBuilder<T> queryBuilder, String patternText, Method m) {
method = m; method = m;
useBestCost = m.getAnnotation(NoCostComputation.class) == null;
Predicate<T> p; Predicate<T> p;
try { try {
@@ -385,6 +399,11 @@ public abstract class QueryRewriter<T> {
} }
} }
@Override
public boolean useBestCost() {
return useBestCost;
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Override @Override
public Predicate<T> rewrite(QueryRewriter<T> rewriter, 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 = private static final Pattern PAT_LABEL =
Pattern.compile("^[a-zA-Z][a-zA-Z0-9]*((=|>=|<=)[+-]?|[+-])\\d+$"); 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_BRANCH = "branch";
public static final String FIELD_CHANGE = "change"; public static final String FIELD_CHANGE = "change";
public static final String FIELD_COMMIT = "commit"; public static final String FIELD_COMMIT = "commit";
@@ -118,6 +119,11 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
return dbProvider; return dbProvider;
} }
@Operator
public Predicate<ChangeData> age(String value) {
return new AgePredicate(dbProvider, value);
}
@Operator @Operator
public Predicate<ChangeData> change(String query) { public Predicate<ChangeData> change(String query) {
if (PAT_LEGACY_ID.matcher(query).matches()) { 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.Change;
import com.google.gerrit.reviewdb.ChangeAccess; import com.google.gerrit.reviewdb.ChangeAccess;
import com.google.gerrit.reviewdb.ReviewDb; import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.query.IntPredicate; import com.google.gerrit.server.query.IntPredicate;
import com.google.gerrit.server.query.Predicate; import com.google.gerrit.server.query.Predicate;
@@ -60,16 +61,19 @@ public class ChangeQueryRewriter extends QueryRewriter<ChangeData> {
} }
@Rewrite("-status:open") @Rewrite("-status:open")
@NoCostComputation
public Predicate<ChangeData> r00_notOpen() { public Predicate<ChangeData> r00_notOpen() {
return ChangeStatusPredicate.closed(dbProvider); return ChangeStatusPredicate.closed(dbProvider);
} }
@Rewrite("-status:closed") @Rewrite("-status:closed")
@NoCostComputation
public Predicate<ChangeData> r00_notClosed() { public Predicate<ChangeData> r00_notClosed() {
return ChangeStatusPredicate.open(dbProvider); return ChangeStatusPredicate.open(dbProvider);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@NoCostComputation
@Rewrite("-status:merged") @Rewrite("-status:merged")
public Predicate<ChangeData> r00_notMerged() { public Predicate<ChangeData> r00_notMerged() {
return or(ChangeStatusPredicate.open(dbProvider), return or(ChangeStatusPredicate.open(dbProvider),
@@ -77,12 +81,21 @@ public class ChangeQueryRewriter extends QueryRewriter<ChangeData> {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@NoCostComputation
@Rewrite("-status:abandoned") @Rewrite("-status:abandoned")
public Predicate<ChangeData> r00_notAbandoned() { public Predicate<ChangeData> r00_notAbandoned() {
return or(ChangeStatusPredicate.open(dbProvider), return or(ChangeStatusPredicate.open(dbProvider),
new ChangeStatusPredicate(dbProvider, Change.Status.MERGED)); 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:*)") @Rewrite("status:open P=(project:*) S=(sortkey_after:*) L=(limit:*)")
public Predicate<ChangeData> r10_byProjectOpenPrev( public Predicate<ChangeData> r10_byProjectOpenPrev(
@Named("P") final ProjectPredicate p, @Named("P") final ProjectPredicate p,