Merge "ChangeScreen2: linkify change author"
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
index 9debd5d..c7f6d16 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -923,7 +923,7 @@
     Boolean reviewed;
     Boolean mergeable;
 
-    String _sortkey;
+    public String _sortkey;
     public int _number;
 
     AccountInfo owner;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
index 4d8c951..414715d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/index/IndexRewriteImpl.java
@@ -147,9 +147,8 @@
       return sqlRewriter.rewrite(in);
     }
     in = basicRewrites.rewrite(in);
-    // Add 1 to specified limit to match behavior of QueryProcessor.
     int limit = ChangeQueryBuilder.hasLimit(in)
-        ? ChangeQueryBuilder.getLimit(in) + 1
+        ? ChangeQueryBuilder.getLimit(in)
         : MAX_LIMIT;
 
     Predicate<ChangeData> out = rewriteImpl(in, index, limit);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
index 24186b8..44c71a9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
@@ -101,6 +101,10 @@
     queries.add(query);
   }
 
+  public String getQuery(int i) {
+    return queries.get(i);
+  }
+
   @Override
   public Object apply(TopLevelResource rsrc)
       throws BadRequestException, AuthException, OrmException {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
index f9bb4f3..d9016e9 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/index/IndexRewriteTest.java
@@ -152,7 +152,7 @@
     Predicate<ChangeData> out = rewrite(in);
     assertSame(AndSource.class, out.getClass());
     assertEquals(ImmutableList.of(
-          query(in.getChild(0), 4),
+          query(in.getChild(0), 3),
           in.getChild(1)),
         out.getChildren());
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index 87fdb18..1e8b87a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -23,6 +23,7 @@
 import com.google.common.base.Charsets;
 import com.google.common.base.Objects;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
 import com.google.common.hash.Hashing;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.restapi.TopLevelResource;
@@ -400,16 +401,171 @@
   @Test
   public void limit() throws Exception {
     TestRepository<InMemoryRepository> repo = createProject("repo");
-    Change change1 = newChange(repo, null, null, null, null).insert();
+    Change last = null;
+    int n = 5;
+    for (int i = 0; i < n; i++) {
+      last = newChange(repo, null, null, null, null).insert();
+    }
+
+    List<ChangeInfo> results;
+    for (int i = 1; i <= n + 2; i++) {
+      results = query("status:new limit:" + i);
+      assertEquals(Math.min(i, n), results.size());
+      assertResultEquals(last, results.get(0));
+    }
+  }
+
+  @Test
+  public void pagination() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    List<Change> changes = Lists.newArrayList();
+    for (int i = 0; i < 5; i++) {
+      changes.add(newChange(repo, null, null, null, null).insert());
+    }
+
+    // Page forward and back through 3 pages of results.
+    QueryChanges q;
+    List<ChangeInfo> results;
+    results = query("status:new limit:2");
+    assertEquals(2, results.size());
+    assertResultEquals(changes.get(4), results.get(0));
+    assertResultEquals(changes.get(3), results.get(1));
+
+    q = newQuery("status:new limit:2");
+    q.setSortKeyBefore(results.get(1)._sortkey);
+    results = query(q);
+    assertEquals(2, results.size());
+    assertResultEquals(changes.get(2), results.get(0));
+    assertResultEquals(changes.get(1), results.get(1));
+
+    q = newQuery("status:new limit:2");
+    q.setSortKeyBefore(results.get(1)._sortkey);
+    results = query(q);
+    assertEquals(1, results.size());
+    assertResultEquals(changes.get(0), results.get(0));
+
+    q = newQuery("status:new limit:2");
+    q.setSortKeyAfter(results.get(0)._sortkey);
+    results = query(q);
+    assertEquals(2, results.size());
+    assertResultEquals(changes.get(2), results.get(0));
+    assertResultEquals(changes.get(1), results.get(1));
+
+    q = newQuery("status:new limit:2");
+    q.setSortKeyAfter(results.get(0)._sortkey);
+    results = query(q);
+    assertEquals(2, results.size());
+    assertResultEquals(changes.get(4), results.get(0));
+    assertResultEquals(changes.get(3), results.get(1));
+  }
+
+  @Test
+  public void sortKeyWithMinuteResolution() throws Exception {
+    clockStepMs = MILLISECONDS.convert(2, MINUTES);
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins1 = newChange(repo, null, null, null, null);
+    Change change1 = ins1.insert();
+    ChangeControl ctl1 = changeControlFactory.controlFor(change1, user);
     Change change2 = newChange(repo, null, null, null, null).insert();
 
-    // TODO(dborowitz): Limit is broken in SQL (returns N+1 results) with some
-    // kinds of predicates but not others.
-    //assertResultEquals(change2, queryOne("status:new limit:1"));
-    List<ChangeInfo> results = query("status:new limit:2");
+    assertTrue(lastUpdatedMs(change1) < lastUpdatedMs(change2));
+
+    List<ChangeInfo> results;
+    results = query("status:new");
     assertEquals(2, results.size());
     assertResultEquals(change2, results.get(0));
     assertResultEquals(change1, results.get(1));
+
+    PostReview.Input input = new PostReview.Input();
+    input.message = "toplevel";
+    postReview.apply(new RevisionResource(
+        new ChangeResource(ctl1), ins1.getPatchSet()), input);
+    change1 = db.changes().get(change1.getId());
+
+    assertTrue(lastUpdatedMs(change1) > lastUpdatedMs(change2));
+    assertTrue(lastUpdatedMs(change1) - lastUpdatedMs(change2)
+        > MILLISECONDS.convert(1, MINUTES));
+
+    results = query("status:new");
+    assertEquals(2, results.size());
+    // change1 moved to the top.
+    assertResultEquals(change1, results.get(0));
+    assertResultEquals(change2, results.get(1));
+  }
+
+  @Test
+  public void sortKeyWithSubMinuteResolution() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    ChangeInserter ins1 = newChange(repo, null, null, null, null);
+    Change change1 = ins1.insert();
+    ChangeControl ctl1 = changeControlFactory.controlFor(change1, user);
+    Change change2 = newChange(repo, null, null, null, null).insert();
+
+    assertTrue(lastUpdatedMs(change1) < lastUpdatedMs(change2));
+
+    List<ChangeInfo> results;
+    results = query("status:new");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+
+    PostReview.Input input = new PostReview.Input();
+    input.message = "toplevel";
+    postReview.apply(new RevisionResource(
+        new ChangeResource(ctl1), ins1.getPatchSet()), input);
+    change1 = db.changes().get(change1.getId());
+
+    assertTrue(lastUpdatedMs(change1) > lastUpdatedMs(change2));
+    assertTrue(lastUpdatedMs(change1) - lastUpdatedMs(change2)
+        < MILLISECONDS.convert(1, MINUTES));
+
+    results = query("status:new");
+    assertEquals(2, results.size());
+    // Same order as before change1 was modified.
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+  }
+
+  @Test
+  public void sortKeyBreaksTiesOnChangeId() throws Exception {
+    clockStepMs = 0;
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change1 = newChange(repo, null, null, null, null).insert();
+    Change change2 = newChange(repo, null, null, null, null).insert();
+    assertEquals(change1.getLastUpdatedOn(), change2.getLastUpdatedOn());
+
+    List<ChangeInfo> results = query("status:new");
+    assertEquals(2, results.size());
+    assertResultEquals(change2, results.get(0));
+    assertResultEquals(change1, results.get(1));
+  }
+
+  @Test
+  public void filterOutMoreThanOnePageOfResults() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change = newChange(repo, null, null, userId.get(), null).insert();
+    int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
+        .getAccountId().get();
+    for (int i = 0; i < 5; i++) {
+      newChange(repo, null, null, user2, null).insert();
+    }
+
+    assertResultEquals(change, queryOne("status:new ownerin:Administrators"));
+    assertResultEquals(change,
+        queryOne("status:new ownerin:Administrators limit:2"));
+  }
+
+  @Test
+  public void filterOutAllResults() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    int user2 = accountManager.authenticate(AuthRequest.forUser("anotheruser"))
+        .getAccountId().get();
+    for (int i = 0; i < 5; i++) {
+      newChange(repo, null, null, user2, null).insert();
+    }
+
+    assertTrue(query("status:new ownerin:Administrators").isEmpty());
+    assertTrue(query("status:new ownerin:Administrators limit:2").isEmpty());
   }
 
   protected ChangeInserter newChange(
@@ -465,25 +621,33 @@
         repoManager.openRepository(new Project.NameKey(name)));
   }
 
-  @SuppressWarnings({"rawtypes", "unchecked"})
-  protected List<ChangeInfo> query(Object query) throws Exception {
+  protected QueryChanges newQuery(Object query) {
     QueryChanges q = queryProvider.get();
     q.addQuery(query.toString());
+    return q;
+  }
+
+  @SuppressWarnings({"rawtypes", "unchecked"})
+  protected List<ChangeInfo> query(QueryChanges q) throws Exception {
     Object result = q.apply(TLR);
     assertTrue(
         String.format("expected List<ChangeInfo>, found %s for [%s]",
-          result, query),
+          result, q.getQuery(0)),
         result instanceof List);
     List results = (List) result;
     if (!results.isEmpty()) {
       assertTrue(
           String.format("expected ChangeInfo, found %s for [%s]",
-            result, query),
+            result, q.getQuery(0)),
           results.get(0) instanceof ChangeInfo);
     }
     return (List<ChangeInfo>) result;
   }
 
+  protected List<ChangeInfo> query(Object query) throws Exception {
+    return query(newQuery(query));
+  }
+
   protected ChangeInfo queryOne(Object query) throws Exception {
     List<ChangeInfo> results = query(query);
     assertTrue(
@@ -492,4 +656,8 @@
         results.size() == 1);
     return results.get(0);
   }
+
+  private static long lastUpdatedMs(Change c) {
+    return c.getLastUpdatedOn().getTime();
+  }
 }