Add operators for absolute last-updated-on search

Some users may have been using sortkey to exclude changes older than
a certain date. This is possible but inconvenient to mimic with an
age: query, so provide operators for absolute searches.

The semantics of these operators are similar to those of `git log`,
except for some incompatibilities/quirks of JGit's GitDateParser.

Change-Id: I52fee758052a5644beb3c97bf727f92129245f3f
This commit is contained in:
Dave Borowitz
2014-02-11 16:43:36 -08:00
parent 0f3fa96e7e
commit a0af7febe6
8 changed files with 258 additions and 18 deletions

View File

@@ -64,6 +64,20 @@ to include a unit suffix, for example `age:2d`:
* mon, month, months (`1 month` is treated as `30 days`)
* y, year, years (`1 year` is treated as `365 days`)
[[before_until]]
before:'TIME'/until:'TIME'
+
Changes modified before the given 'TIME', inclusive. With no time,
assumes 00:00:00 in the local time zone. Supports many formats supported
by `git log`.
[[after_since]]
after:'TIME'/since:'TIME'
+
Changes modified before the given 'TIME', inclusive. With no time,
assumes 00:00:00 in the local time zone. Supports many formats supported
by `git log`.
[[change]]
change:'ID'::
+

View File

@@ -45,7 +45,7 @@ import org.apache.lucene.search.TermQuery;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.NumericUtils;
import java.sql.Timestamp;
import java.util.Date;
import java.util.List;
public class QueryBuilder {
@@ -232,7 +232,7 @@ public class QueryBuilder {
return queryBuilder.createPhraseQuery(p.getField().getName(), p.getValue());
}
public static int toIndexTime(Timestamp ts) {
public static int toIndexTime(Date ts) {
return (int) (ts.getTime() / 60000);
}

View File

@@ -14,14 +14,36 @@
package com.google.gerrit.server.index;
import com.google.gerrit.server.query.QueryParseException;
import org.eclipse.jgit.util.GitDateParser;
import org.joda.time.DateTime;
import java.sql.Timestamp;
import java.text.ParseException;
import java.util.Date;
import java.util.Locale;
public abstract class TimestampRangePredicate<I> extends IndexPredicate<I> {
protected static Date parse(String value) throws QueryParseException {
try {
return GitDateParser.parse(value, DateTime.now().toCalendar(Locale.US));
} catch (ParseException e) {
// ParseException's message is specific and helpful, so preserve it.
throw new QueryParseException(e.getMessage(), e);
}
}
protected TimestampRangePredicate(FieldDef<I, Timestamp> def,
String name, String value) {
super(def, name, value);
}
public abstract Timestamp getMinTimestamp();
public abstract Timestamp getMaxTimestamp();
public abstract Date getMinTimestamp();
public abstract Date getMaxTimestamp();
@Override
public int getCost() {
return 1;
}
}

View File

@@ -0,0 +1,46 @@
// 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.server.index.ChangeField;
import com.google.gerrit.server.index.TimestampRangePredicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gwtorm.server.OrmException;
import java.util.Date;
public class AfterPredicate extends TimestampRangePredicate<ChangeData> {
private final Date cut;
AfterPredicate(String value) throws QueryParseException {
super(ChangeField.UPDATED, ChangeQueryBuilder.FIELD_AFTER, value);
cut = parse(value);
}
@Override
public Date getMinTimestamp() {
return cut;
}
@Override
public Date getMaxTimestamp() {
return new Date(Long.MAX_VALUE);
}
@Override
public boolean match(ChangeData cd) throws OrmException {
return cd.change().getLastUpdatedOn().getTime() >= cut.getTime();
}
}

View File

@@ -37,10 +37,12 @@ public class AgePredicate extends TimestampRangePredicate<ChangeData> {
this.cut = TimeUtil.nowMs() - ms;
}
@Override
public Timestamp getMinTimestamp() {
return new Timestamp(0);
}
@Override
public Timestamp getMaxTimestamp() {
return new Timestamp(cut);
}
@@ -54,9 +56,4 @@ public class AgePredicate extends TimestampRangePredicate<ChangeData> {
Change change = object.change();
return change != null && change.getLastUpdatedOn().getTime() <= cut;
}
@Override
public int getCost() {
return 1;
}
}

View File

@@ -0,0 +1,46 @@
// 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.server.index.ChangeField;
import com.google.gerrit.server.index.TimestampRangePredicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gwtorm.server.OrmException;
import java.util.Date;
public class BeforePredicate extends TimestampRangePredicate<ChangeData> {
private final Date cut;
BeforePredicate(String value) throws QueryParseException {
super(ChangeField.UPDATED, ChangeQueryBuilder.FIELD_BEFORE, value);
cut = parse(value);
}
@Override
public Date getMinTimestamp() {
return new Date(0);
}
@Override
public Date getMaxTimestamp() {
return cut;
}
@Override
public boolean match(ChangeData cd) throws OrmException {
return cd.change().getLastUpdatedOn().getTime() <= cut.getTime();
}
}

View File

@@ -84,7 +84,9 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
// NOTE: As new search operations are added, please keep the
// SearchSuggestOracle up to date.
public static final String FIELD_AFTER = "after";
public static final String FIELD_AGE = "age";
public static final String FIELD_BEFORE = "before";
public static final String FIELD_BRANCH = "branch";
public static final String FIELD_CHANGE = "change";
public static final String FIELD_COMMENT = "comment";
@@ -238,6 +240,26 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
return new AgePredicate(value);
}
@Operator
public Predicate<ChangeData> before(String value) throws QueryParseException {
return new BeforePredicate(value);
}
@Operator
public Predicate<ChangeData> until(String value) throws QueryParseException {
return before(value);
}
@Operator
public Predicate<ChangeData> after(String value) throws QueryParseException {
return new AfterPredicate(value);
}
@Operator
public Predicate<ChangeData> since(String value) throws QueryParseException {
return after(value);
}
@Operator
public Predicate<ChangeData> change(String query) {
if (PAT_LEGACY_ID.matcher(query).matches()) {

View File

@@ -15,10 +15,9 @@
package com.google.gerrit.server.query.change;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.HOURS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.MINUTES;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@@ -36,7 +35,6 @@ import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountManager;
@@ -62,6 +60,7 @@ import com.google.inject.util.Providers;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.joda.time.DateTime;
import org.joda.time.DateTimeUtils;
import org.joda.time.DateTimeUtils.MillisProvider;
import org.junit.After;
@@ -70,7 +69,6 @@ import org.junit.Ignore;
import org.junit.Test;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@Ignore
@@ -96,6 +94,8 @@ public abstract class AbstractQueryChangesTest {
protected CurrentUser user;
protected volatile long clockStepMs;
private String systemTimeZone;
protected abstract Injector createInjector();
@Before
@@ -138,11 +138,11 @@ public abstract class AbstractQueryChangesTest {
}
@Before
public void setMillisProvider() {
public void setTimeForTesting() {
systemTimeZone = System.setProperty("user.timezone", "US/Eastern");
clockStepMs = 1;
final AtomicLong clockMs = new AtomicLong(
MILLISECONDS.convert(ChangeUtil.SORT_KEY_EPOCH_MINS, MINUTES)
+ MILLISECONDS.convert(60, DAYS));
new DateTime(2009, 9, 30, 17, 0, 0).getMillis());
DateTimeUtils.setCurrentMillisProvider(new MillisProvider() {
@Override
@@ -153,8 +153,9 @@ public abstract class AbstractQueryChangesTest {
}
@After
public void resetMillisProvider() {
public void resetTime() {
DateTimeUtils.setCurrentMillisSystem();
System.setProperty("user.timezone", systemTimeZone);
}
@Test
@@ -665,7 +666,7 @@ public abstract class AbstractQueryChangesTest {
@Test
public void byAge() throws Exception {
long thirtyHours = MILLISECONDS.convert(30, TimeUnit.HOURS);
long thirtyHours = MILLISECONDS.convert(30, HOURS);
clockStepMs = thirtyHours;
TestRepository<InMemoryRepository> repo = createProject("repo");
Change change1 = newChange(repo, null, null, null, null).insert();
@@ -695,6 +696,98 @@ public abstract class AbstractQueryChangesTest {
assertResultEquals(change1, results.get(1));
}
@Test
public void byBeforeAbsolute() throws Exception {
clockStepMs = MILLISECONDS.convert(30, HOURS);
TestRepository<InMemoryRepository> repo = createProject("repo");
Change change1 = newChange(repo, null, null, null, null).insert();
Change change2 = newChange(repo, null, null, null, null).insert();
clockStepMs = 0;
// GitDateParser drops unparsed portions of the string, so be very careful
// with formats.
assertTrue(query("before:2009-09-29").isEmpty());
assertTrue(query("before:2009-09-30").isEmpty());
assertTrue(query("before:\"2009-09-30 16:59:00 -0400\"").isEmpty());
assertResultEquals(change1,
queryOne("before:\"2009-09-30 21:02:00 -0400\""));
assertResultEquals(change1, queryOne("before:2009-10-01"));
List<ChangeInfo> results;
results = query("before:2009-10-03");
assertEquals(2, results.size());
assertResultEquals(change2, results.get(0));
assertResultEquals(change1, results.get(1));
}
@Test
public void byBeforeRelative() throws Exception {
clockStepMs = MILLISECONDS.convert(30, HOURS);
TestRepository<InMemoryRepository> repo = createProject("repo");
Change change1 = newChange(repo, null, null, null, null).insert();
Change change2 = newChange(repo, null, null, null, null).insert();
clockStepMs = 0;
assertTrue(query("before:\"3 days ago\"").isEmpty());
assertResultEquals(change1, queryOne("before:\"2 days ago\""));
List<ChangeInfo> results;
results = query("before:\"1 day ago\"");
assertEquals(2, results.size());
assertResultEquals(change2, results.get(0));
assertResultEquals(change1, results.get(1));
results = query("before:\"12 hours ago\"");
assertEquals(2, results.size());
assertResultEquals(change2, results.get(0));
assertResultEquals(change1, results.get(1));
}
@Test
public void byAfterAbsolute() throws Exception {
clockStepMs = MILLISECONDS.convert(30, HOURS);
TestRepository<InMemoryRepository> repo = createProject("repo");
Change change1 = newChange(repo, null, null, null, null).insert();
Change change2 = newChange(repo, null, null, null, null).insert();
clockStepMs = 0;
// GitDateParser drops unparsed portions of the string, so be very careful
// with formats.
assertTrue(query("after:2009-10-02").isEmpty());
assertResultEquals(change2,
queryOne("after:\"2009-10-01 20:59:59 -0400\""));
assertResultEquals(change2, queryOne("after:2009-10-01"));
List<ChangeInfo> results;
results = query("after:2009-09-30");
assertEquals(2, results.size());
assertResultEquals(change2, results.get(0));
assertResultEquals(change1, results.get(1));
}
@Test
public void byAfterRelative() throws Exception {
clockStepMs = MILLISECONDS.convert(30, HOURS);
TestRepository<InMemoryRepository> repo = createProject("repo");
Change change1 = newChange(repo, null, null, null, null).insert();
Change change2 = newChange(repo, null, null, null, null).insert();
clockStepMs = 0;
assertTrue(query("after:\"1 days ago\"").isEmpty());
assertResultEquals(change2, queryOne("after:\"2 days ago\""));
List<ChangeInfo> results;
results = query("after:\"3 days ago\"");
assertEquals(2, results.size());
assertResultEquals(change2, results.get(0));
assertResultEquals(change1, results.get(1));
results = query("after:\"72 hours ago\"");
assertEquals(2, results.size());
assertResultEquals(change2, results.get(0));
assertResultEquals(change1, results.get(1));
}
protected ChangeInserter newChange(
TestRepository<InMemoryRepository> repo,
@Nullable RevCommit commit, @Nullable String key, @Nullable Integer owner,