Support change~branch~id in query syntax

It is odd that we describe this in the REST API as a canonical way of
referring to a change, but don't support it in the query syntax. Since
the query parser is also used by the /r handler, this fixes that there
as well.

Unlike in the ChangesCollection parser, allow prefix searches for the
id portion, to be consistent with other search operators.

Change-Id: I55e1cc33caf907cb0ff17dae7a81a46156b6f562
diff --git a/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g b/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g
index 4be4ab6..98f1af9 100644
--- a/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g
+++ b/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g
@@ -188,6 +188,6 @@
      | '?'
      | '[' | ']'
      | '{' | '}'
-     | '~'
+     // | '~' permit
      )
   ;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 5c39df8..b8d1241 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Optional;
 import com.google.common.collect.Lists;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.common.data.GroupReference;
@@ -30,6 +31,7 @@
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupBackends;
+import com.google.gerrit.server.change.ChangeTriplet;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.TrackingFooters;
@@ -71,8 +73,8 @@
   private static final Pattern PAT_LEGACY_ID = Pattern.compile("^[1-9][0-9]*$");
   private static final Pattern PAT_CHANGE_ID =
       Pattern.compile("^[iI][0-9a-f]{4,}.*$");
-  private static final Pattern DEF_CHANGE =
-      Pattern.compile("^([1-9][0-9]*|[iI][0-9a-f]{4,}.*)$");
+  private static final Pattern DEF_CHANGE = Pattern.compile(
+      "^(?:[1-9][0-9]*|(?:[^~]+~[^~]+~)?[iI][0-9a-f]{4,}.*)$");
 
   // NOTE: As new search operations are added, please keep the
   // SearchSuggestOracle up to date.
@@ -260,15 +262,21 @@
   }
 
   @Operator
-  public Predicate<ChangeData> change(String query) {
+  public Predicate<ChangeData> change(String query) throws QueryParseException {
     if (PAT_LEGACY_ID.matcher(query).matches()) {
       return new LegacyChangeIdPredicate(args, Change.Id.parse(query));
-
     } else if (PAT_CHANGE_ID.matcher(query).matches()) {
       return new ChangeIdPredicate(args, parseChangeId(query));
     }
+    Optional<ChangeTriplet> triplet = ChangeTriplet.parse(query);
+    if (triplet.isPresent()) {
+      return Predicate.and(
+          project(triplet.get().project().get()),
+          branch(triplet.get().branch().get()),
+          new ChangeIdPredicate(args, parseChangeId(triplet.get().id().get())));
+    }
 
-    throw new IllegalArgumentException();
+    throw new QueryParseException("Invalid change format");
   }
 
   @Operator
@@ -713,7 +721,11 @@
     if (query.startsWith("refs/")) {
       return ref(query);
     } else if (DEF_CHANGE.matcher(query).matches()) {
-      return change(query);
+      try {
+        return change(query);
+      } catch (QueryParseException e) {
+        // Skip.
+      }
     }
 
     List<Predicate<ChangeData>> predicates = Lists.newArrayListWithCapacity(9);
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 825a008..84095f8 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
@@ -50,6 +50,7 @@
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.change.ChangeInserter;
+import com.google.gerrit.server.change.ChangeTriplet;
 import com.google.gerrit.server.change.ChangesCollection;
 import com.google.gerrit.server.change.PostReview;
 import com.google.gerrit.server.change.RevisionResource;
@@ -207,6 +208,33 @@
   }
 
   @Test
+  public void byTriplet() throws Exception {
+    TestRepository<InMemoryRepository> repo = createProject("repo");
+    Change change = newChange(repo, null, null, null, "branch").insert();
+    String k = change.getKey().get();
+
+    assertResultEquals(change, queryOne("repo~branch~" + k));
+    assertResultEquals(change, queryOne("change:repo~branch~" + k));
+    assertResultEquals(change, queryOne("repo~refs/heads/branch~" + k));
+    assertResultEquals(change, queryOne("change:repo~refs/heads/branch~" + k));
+    assertResultEquals(change, queryOne("repo~branch~" + k.substring(0, 10)));
+    assertResultEquals(change,
+        queryOne("change:repo~branch~" + k.substring(0, 10)));
+
+    assertThat(query("foo~bar")).isEmpty();
+    assertBadQuery("change:foo~bar");
+    assertThat(query("otherrepo~branch~" + k)).isEmpty();
+    assertThat(query("change:otherrepo~branch~" + k)).isEmpty();
+    assertThat(query("repo~otherbranch~" + k)).isEmpty();
+    assertThat(query("change:repo~otherbranch~" + k)).isEmpty();
+    assertThat(query("repo~branch~I0000000000000000000000000000000000000000"))
+        .isEmpty();
+    assertThat(query(
+          "change:repo~branch~I0000000000000000000000000000000000000000"))
+        .isEmpty();
+  }
+
+  @Test
   public void byStatus() throws Exception {
     TestRepository<InMemoryRepository> repo = createProject("repo");
     ChangeInserter ins1 = newChange(repo, null, null, null, null);
@@ -990,6 +1018,7 @@
 
     assertResultEquals(change1,
         queryOne(Integer.toString(change1.getId().get())));
+    assertResultEquals(change1, queryOne(ChangeTriplet.format(change1)));
     assertResultEquals(change2, queryOne("foosubject"));
     assertResultEquals(change3, queryOne("Foo.java"));
     assertResultEquals(change4, queryOne("Code-Review+1"));