Merge changes Ibe956014,I4222b894,Iac4ea144,I803717e8,I39e97dcd, ...

* changes:
  Support updating multiple VersionedMetaDatas in a BatchRefUpdate
  Refactor byChange() into two methods: drafts and published
  Add draft comments to PatchLineCommentsUtil
  Fix bug for comments with no range
  Resolve issue with naming of drafts ref
  Add method to parse an AccountId out of a RefName
  Improve PatchSet.Id.fromRef performance and avoid double-parsing
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
index 004b7cf..d4a5b3d 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GetCommitIT.java
@@ -14,85 +14,147 @@
 
 package com.google.gerrit.acceptance.rest.project;
 
-import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
+import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertNull;
 
+import com.google.common.collect.Iterables;
 import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.PushOneCommit;
 import com.google.gerrit.acceptance.RestResponse;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.extensions.restapi.IdString;
-import com.google.gerrit.reviewdb.client.RefNames;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.project.ListBranches.BranchInfo;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 
 import org.apache.http.HttpStatus;
+import org.eclipse.jgit.junit.TestRepository;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.junit.After;
+import org.junit.Before;
 import org.junit.Test;
 
-import java.io.IOException;
-
 public class GetCommitIT extends AbstractDaemonTest {
   @Inject
   private AllProjectsName allProjects;
 
-  @Test
-  public void getCommit() throws Exception {
-    RestResponse r =
-        adminSession.get("/projects/" + project.get() + "/branches/"
-            + IdString.fromDecoded(RefNames.REFS_CONFIG).encoded());
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    BranchInfo branchInfo =
-        newGson().fromJson(r.getReader(), BranchInfo.class);
-    r.consume();
+  @Inject
+  private GitRepositoryManager repoManager;
 
-    allow(Permission.READ, ANONYMOUS_USERS, branchInfo.ref);
+  @Inject
+  private ProjectCache projectCache;
 
-    r = adminSession.get("/projects/" + project.get() + "/commits/"
-        + branchInfo.revision);
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    CommitInfo commitInfo =
-        newGson().fromJson(r.getReader(), CommitInfo.class);
-    assertEquals(branchInfo.revision, commitInfo.commit);
-    assertEquals("Created project", commitInfo.subject);
-    assertEquals("Created project\n", commitInfo.message);
-    assertNotNull(commitInfo.author);
-    assertEquals("Administrator", commitInfo.author.name);
-    assertNotNull(commitInfo.committer);
-    assertEquals("Gerrit Code Review", commitInfo.committer.name);
-    assertTrue(commitInfo.parents.isEmpty());
-  }
+  private TestRepository<Repository> repo;
 
-  @Test
-  public void getNonExistingCommit_NotFound() throws IOException {
-    RestResponse r = adminSession.get("/projects/" + project.get() + "/commits/"
-        + ObjectId.zeroId().name());
-    assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
-  }
+  @Before
+  public void setUp() throws Exception {
+    repo = new TestRepository<>(repoManager.openRepository(project));
 
-  @Test
-  public void getNonVisibleCommit_NotFound() throws Exception {
-    RestResponse r =
-        adminSession.get("/projects/" + project.get() + "/branches/"
-            + IdString.fromDecoded(RefNames.REFS_CONFIG).encoded());
-    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
-    BranchInfo branchInfo =
-        newGson().fromJson(r.getReader(), BranchInfo.class);
-    r.consume();
-
-    ProjectConfig cfg = projectCache.checkedGet(allProjects).getConfig();
-    for (AccessSection sec : cfg.getAccessSections()) {
+    ProjectConfig pc = projectCache.checkedGet(allProjects).getConfig();
+    for (AccessSection sec : pc.getAccessSections()) {
       sec.removePermission(Permission.READ);
     }
-    saveProjectConfig(allProjects, cfg);
+    saveProjectConfig(allProjects, pc);
+  }
 
-    r = adminSession.get("/projects/" + project.get() + "/commits/"
-        + branchInfo.revision);
+  @After
+  public void tearDown() throws Exception {
+    if (repo != null) {
+      repo.getRepository().close();
+    }
+  }
+
+  @Test
+  public void getNonExistingCommit_NotFound() throws Exception {
+    assertNotFound(
+        ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"));
+  }
+
+  @Test
+  public void getMergedCommit_Found() throws Exception {
+    allow(Permission.READ, REGISTERED_USERS, "refs/heads/*");
+    RevCommit commit = repo.parseBody(repo.branch("master")
+        .commit()
+        .message("Create\n\nNew commit\n")
+        .create());
+
+    CommitInfo info = getCommit(commit);
+    assertEquals(commit.name(), info.commit);
+    assertEquals("Create", info.subject);
+    assertEquals("Create\n\nNew commit\n", info.message);
+    assertEquals("J. Author", info.author.name);
+    assertEquals("jauthor@example.com", info.author.email);
+    assertEquals("J. Committer", info.committer.name);
+    assertEquals("jcommitter@example.com", info.committer.email);
+
+    CommitInfo parent = Iterables.getOnlyElement(info.parents);
+    assertEquals(commit.getParent(0).name(), parent.commit);
+    assertEquals("Initial empty repository", parent.subject);
+    assertNull(parent.message);
+    assertNull(parent.author);
+    assertNull(parent.committer);
+  }
+
+  @Test
+  public void getMergedCommit_NotFound() throws Exception {
+    RevCommit commit = repo.parseBody(repo.branch("master")
+        .commit()
+        .message("Create\n\nNew commit\n")
+        .create());
+    assertNotFound(commit);
+  }
+
+  @Test
+  public void getOpenChange_Found() throws Exception {
+    allow(Permission.READ, REGISTERED_USERS, "refs/heads/*");
+    PushOneCommit.Result r = pushFactory.create(db, admin.getIdent())
+        .to(git, "refs/for/master");
+    r.assertOkStatus();
+
+    CommitInfo info = getCommit(r.getCommitId());
+    assertEquals(r.getCommitId().name(), info.commit);
+    assertEquals("test commit", info.subject);
+    assertEquals("test commit\n\nChange-Id: " + r.getChangeId() + "\n",
+        info.message);
+    assertEquals("admin", info.author.name);
+    assertEquals("admin@example.com", info.author.email);
+    assertEquals("admin", info.committer.name);
+    assertEquals("admin@example.com", info.committer.email);
+
+    CommitInfo parent = Iterables.getOnlyElement(info.parents);
+    assertEquals(r.getCommit().getParent(0).name(), parent.commit);
+    assertEquals("Initial empty repository", parent.subject);
+    assertNull(parent.message);
+    assertNull(parent.author);
+    assertNull(parent.committer);
+  }
+
+  @Test
+  public void getOpenChange_NotFound() throws Exception {
+    PushOneCommit.Result r = pushFactory.create(db, admin.getIdent())
+        .to(git, "refs/for/master");
+    r.assertOkStatus();
+    assertNotFound(r.getCommitId());
+  }
+
+  private void assertNotFound(ObjectId id) throws Exception {
+    RestResponse r = userSession.get(
+        "/projects/" + project.get() + "/commits/" + id.name());
     assertEquals(HttpStatus.SC_NOT_FOUND, r.getStatusCode());
   }
+
+  private CommitInfo getCommit(ObjectId id) throws Exception {
+    RestResponse r = userSession.get(
+        "/projects/" + project.get() + "/commits/" + id.name());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    CommitInfo result = newGson().fromJson(r.getReader(), CommitInfo.class);
+    r.consume();
+    return result;
+  }
 }
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index 923d752..bb3d2e7 100644
--- a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -121,6 +121,11 @@
     Project.NameKey projectAwesome = new Project.NameKey("project-awesome");
     createProject(sshSession, projectAwesome.get());
 
+    assertEquals(HttpStatus.SC_BAD_REQUEST,
+        GET("/projects/?p=some&r=.*").getStatusCode());
+    assertEquals(HttpStatus.SC_BAD_REQUEST,
+        GET("/projects/?p=some&m=some").getStatusCode());
+
     RestResponse r = GET("/projects/?p=some");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
@@ -138,13 +143,22 @@
     Project.NameKey projectAwesome = new Project.NameKey("project-awesome");
     createProject(sshSession, projectAwesome.get());
 
+    assertEquals(HttpStatus.SC_BAD_REQUEST,
+        GET("/projects/?r=[.*some").getStatusCode());
+    assertEquals(HttpStatus.SC_BAD_REQUEST,
+        GET("/projects/?r=.*&p=s").getStatusCode());
+    assertEquals(HttpStatus.SC_BAD_REQUEST,
+        GET("/projects/?r=.*&m=s").getStatusCode());
+
     RestResponse r = GET("/projects/?r=.*some");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
     assertProjects(Arrays.asList(projectAwesome), result.values());
 
-    r = GET("/projects/?r=[.*some");
-    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
+    r = GET("/projects/?r=some-project$");
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    result = toProjectInfoMap(r);
+    assertProjects(Arrays.asList(someProject), result.values());
 
     r = GET("/projects/?r=.*");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
@@ -181,6 +195,11 @@
     Project.NameKey projectAwesome = new Project.NameKey("project-awesome");
     createProject(sshSession, projectAwesome.get());
 
+    assertEquals(HttpStatus.SC_BAD_REQUEST,
+        GET("/projects/?m=some&r=.*").getStatusCode());
+    assertEquals(HttpStatus.SC_BAD_REQUEST,
+        GET("/projects/?m=some&p=some").getStatusCode());
+
     RestResponse r = GET("/projects/?m=some");
     assertEquals(HttpStatus.SC_OK, r.getStatusCode());
     Map<String, ProjectInfo> result = toProjectInfoMap(r);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CommonConverters.java b/gerrit-server/src/main/java/com/google/gerrit/server/CommonConverters.java
new file mode 100644
index 0000000..09c2ea6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CommonConverters.java
@@ -0,0 +1,42 @@
+// 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;
+
+import com.google.gerrit.extensions.common.GitPerson;
+
+import org.eclipse.jgit.lib.PersonIdent;
+
+import java.sql.Timestamp;
+
+/**
+ * Converters to classes in {@link com.google.gerrit.extensions.common}.
+ * <p>
+ * The server frequently needs to convert internal types to types exposed in the
+ * extension API, but the converters themselves are not part of this API. This
+ * class contains such converters as static utility methods.
+ */
+public class CommonConverters {
+  public static GitPerson toGitPerson(PersonIdent ident) {
+    GitPerson result = new GitPerson();
+    result.name = ident.getName();
+    result.email = ident.getEmailAddress();
+    result.date = new Timestamp(ident.getWhen().getTime());
+    result.tz = ident.getTimeZoneOffset();
+    return result;
+  }
+
+  private CommonConverters() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
index 0ccb15c..fd727c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetRelated.java
@@ -21,12 +21,12 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.CommitInfo;
-import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CommonConverters;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectControl;
@@ -39,7 +39,6 @@
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -50,7 +49,6 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.LinkedList;
@@ -272,15 +270,6 @@
     return r;
   }
 
-  private static GitPerson toGitPerson(PersonIdent id) {
-    GitPerson p = new GitPerson();
-    p.name = id.getName();
-    p.email = id.getEmailAddress();
-    p.date = new Timestamp(id.getWhen().getTime());
-    p.tz = id.getTimeZoneOffset();
-    return p;
-  }
-
   public static class RelatedInfo {
     public List<ChangeAndCommit> changes;
   }
@@ -309,7 +298,7 @@
         p.commit = c.getParent(i).name();
         commit.parents.add(p);
       }
-      commit.author = toGitPerson(c.getAuthorIdent());
+      commit.author = CommonConverters.toGitPerson(c.getAuthorIdent());
       commit.subject = c.getShortMessage();
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetCommit.java
index 0d07ce8..a857914 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetCommit.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.extensions.common.CommitInfo;
 import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CommonConverters;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.PersonIdent;
@@ -36,8 +37,8 @@
   private static CommitInfo toCommitInfo(RevCommit commit) {
     CommitInfo info = new CommitInfo();
     info.commit = commit.getName();
-    info.author = toGitPerson(commit.getAuthorIdent());
-    info.committer = toGitPerson(commit.getCommitterIdent());
+    info.author = CommonConverters.toGitPerson(commit.getAuthorIdent());
+    info.committer = CommonConverters.toGitPerson(commit.getCommitterIdent());
     info.subject = commit.getShortMessage();
     info.message = commit.getFullMessage();
     info.parents = new ArrayList<>(commit.getParentCount());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
index 1440200..7e52381 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetReflog.java
@@ -20,12 +20,12 @@
 import com.google.gerrit.extensions.restapi.AuthException;
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CommonConverters;
 import com.google.gerrit.server.args4j.TimestampHandler;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.ReflogEntry;
 import org.eclipse.jgit.lib.ReflogReader;
 import org.eclipse.jgit.lib.Repository;
@@ -122,14 +122,7 @@
     public ReflogEntryInfo(ReflogEntry e) {
       oldId = e.getOldId().getName();
       newId = e.getNewId().getName();
-
-      PersonIdent ident = e.getWho();
-      who = new GitPerson();
-      who.name = ident.getName();
-      who.email = ident.getEmailAddress();
-      who.date = new Timestamp(ident.getWhen().getTime());
-      who.tz = ident.getTimeZoneOffset();
-
+      who = CommonConverters.toGitPerson(e.getWho());
       comment = e.getComment();
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index a28ee40..8b895f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Predicate;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -38,14 +39,12 @@
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.util.RegexListSearcher;
 import com.google.gerrit.server.util.TreeFormatter;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import dk.brics.automaton.RegExp;
-import dk.brics.automaton.RunAutomaton;
-
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
@@ -449,8 +448,10 @@
 
   private Iterable<Project.NameKey> scan() throws BadRequestException {
     if (matchPrefix != null) {
+      checkMatchOptions(matchSubstring == null && matchRegex == null);
       return projectCache.byName(matchPrefix);
     } else if (matchSubstring != null) {
+      checkMatchOptions(matchPrefix == null && matchRegex == null);
       return Iterables.filter(projectCache.all(),
           new Predicate<Project.NameKey>() {
             public boolean apply(Project.NameKey in) {
@@ -459,32 +460,31 @@
             }
           });
     } else if (matchRegex != null) {
-      if (matchRegex.startsWith("^")) {
-        matchRegex = matchRegex.substring(1);
-        if (matchRegex.endsWith("$") && !matchRegex.endsWith("\\$")) {
-          matchRegex = matchRegex.substring(0, matchRegex.length() - 1);
-        }
-      }
-      if (matchRegex.equals(".*")) {
-        return projectCache.all();
-      }
+      checkMatchOptions(matchPrefix == null && matchSubstring == null);
+      RegexListSearcher<Project.NameKey> searcher;
       try {
-        final RunAutomaton a =
-            new RunAutomaton(new RegExp(matchRegex).toAutomaton());
-        return Iterables.filter(projectCache.all(),
-            new Predicate<Project.NameKey>() {
-              public boolean apply(Project.NameKey in) {
-                return a.run(in.get());
-              }
-            });
+        searcher = new RegexListSearcher<Project.NameKey>(matchRegex) {
+          @Override
+          public String apply(Project.NameKey in) {
+            return in.get();
+          }
+        };
       } catch (IllegalArgumentException e) {
         throw new BadRequestException(e.getMessage());
       }
+      return searcher.search(ImmutableList.copyOf(projectCache.all()));
     } else {
       return projectCache.all();
     }
   }
 
+  private static void checkMatchOptions(boolean cond)
+      throws BadRequestException {
+    if (!cond) {
+      throw new BadRequestException("specify exactly one of p/m/r");
+    }
+  }
+
   private void printProjectTree(final PrintWriter stdout,
       final TreeMap<Project.NameKey, ProjectNode> treeMap) {
     final SortedSet<ProjectNode> sortedNodes = new TreeSet<>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
index d073002..3d6f8b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexPathPredicate.java
@@ -16,76 +16,21 @@
 
 import com.google.gerrit.server.index.ChangeField;
 import com.google.gerrit.server.index.RegexPredicate;
+import com.google.gerrit.server.util.RegexListSearcher;
 import com.google.gwtorm.server.OrmException;
 
-import dk.brics.automaton.Automaton;
-import dk.brics.automaton.RegExp;
-import dk.brics.automaton.RunAutomaton;
-
-import java.util.Collections;
 import java.util.List;
 
 class RegexPathPredicate extends RegexPredicate<ChangeData> {
-  private final RunAutomaton pattern;
-
-  private final String prefixBegin;
-  private final String prefixEnd;
-  private final int prefixLen;
-  private final boolean prefixOnly;
-
   RegexPathPredicate(String fieldName, String re) {
     super(ChangeField.PATH, re);
-
-    if (re.startsWith("^")) {
-      re = re.substring(1);
-    }
-
-    if (re.endsWith("$") && !re.endsWith("\\$")) {
-      re = re.substring(0, re.length() - 1);
-    }
-
-    Automaton automaton = new RegExp(re).toAutomaton();
-    prefixBegin = automaton.getCommonPrefix();
-    prefixLen = prefixBegin.length();
-
-    if (0 < prefixLen) {
-      char max = (char) (prefixBegin.charAt(prefixLen - 1) + 1);
-      prefixEnd = prefixBegin.substring(0, prefixLen - 1) + max;
-      prefixOnly = re.equals(prefixBegin + ".*");
-    } else {
-      prefixEnd = "";
-      prefixOnly = false;
-    }
-
-    pattern = prefixOnly ? null : new RunAutomaton(automaton);
   }
 
   @Override
   public boolean match(ChangeData object) throws OrmException {
     List<String> files = object.currentFilePaths();
     if (files != null) {
-      int begin, end;
-
-      if (0 < prefixLen) {
-        begin = find(files, prefixBegin);
-        end = find(files, prefixEnd);
-      } else {
-        begin = 0;
-        end = files.size();
-      }
-
-      if (prefixOnly) {
-        return begin < end;
-      }
-
-      while (begin < end) {
-        if (pattern.run(files.get(begin++))) {
-          return true;
-        }
-      }
-
-      return false;
-
+      return RegexListSearcher.ofStrings(getValue()).hasMatch(files);
     } else {
       // The ChangeData can't do expensive lookups right now. Bypass
       // them and include the result anyway. We might be able to do
@@ -95,11 +40,6 @@
     }
   }
 
-  private static int find(List<String> files, String p) {
-    int r = Collections.binarySearch(files, p);
-    return r < 0 ? -(r + 1) : r;
-  }
-
   @Override
   public int getCost() {
     return 1;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RegexListSearcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RegexListSearcher.java
new file mode 100644
index 0000000..4b0fd35
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RegexListSearcher.java
@@ -0,0 +1,112 @@
+// 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.util;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Chars;
+
+import dk.brics.automaton.Automaton;
+import dk.brics.automaton.RegExp;
+import dk.brics.automaton.RunAutomaton;
+
+import java.util.Collections;
+import java.util.List;
+
+/** Helper to search sorted lists for elements matching a regex. */
+public abstract class RegexListSearcher<T> implements Function<T, String> {
+  public static RegexListSearcher<String> ofStrings(String re) {
+    return new RegexListSearcher<String>(re) {
+      @Override
+      public String apply(String in) {
+        return in;
+      }
+    };
+  }
+
+  private final RunAutomaton pattern;
+
+  private final String prefixBegin;
+  private final String prefixEnd;
+  private final int prefixLen;
+  private final boolean prefixOnly;
+
+  public RegexListSearcher(String re) {
+    if (re.startsWith("^")) {
+      re = re.substring(1);
+    }
+
+    if (re.endsWith("$") && !re.endsWith("\\$")) {
+      re = re.substring(0, re.length() - 1);
+    }
+
+    Automaton automaton = new RegExp(re).toAutomaton();
+    prefixBegin = automaton.getCommonPrefix();
+    prefixLen = prefixBegin.length();
+
+    if (0 < prefixLen) {
+      char max = Chars.checkedCast(prefixBegin.charAt(prefixLen - 1) + 1);
+      prefixEnd = prefixBegin.substring(0, prefixLen - 1) + max;
+      prefixOnly = re.equals(prefixBegin + ".*");
+    } else {
+      prefixEnd = "";
+      prefixOnly = false;
+    }
+
+    pattern = prefixOnly ? null : new RunAutomaton(automaton);
+  }
+
+  public Iterable<T> search(List<T> list) {
+    checkNotNull(list);
+    int begin, end;
+
+    if (0 < prefixLen) {
+      // Assumes many consecutive elements may have the same prefix, so the cost
+      // of two binary searches is less than iterating to find the endpoints.
+      begin = find(list, prefixBegin);
+      end = find(list, prefixEnd);
+    } else {
+      begin = 0;
+      end = list.size();
+    }
+
+    if (prefixOnly) {
+      return begin < end ? list.subList(begin, end) : ImmutableList.<T> of();
+    }
+
+    return Iterables.filter(
+        list.subList(begin, end),
+        new Predicate<T>() {
+          @Override
+          public boolean apply(T in) {
+            return pattern.run(RegexListSearcher.this.apply(in));
+          }
+        });
+  }
+
+  public boolean hasMatch(List<T> list) {
+    return !Iterables.isEmpty(search(list));
+  }
+
+  private int find(List<T> list, String p) {
+    int r = Collections.binarySearch(Lists.transform(list, this), p);
+    return r < 0 ? -(r + 1) : r;
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/RegexListSearcherTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/RegexListSearcherTest.java
new file mode 100644
index 0000000..8f73005
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/RegexListSearcherTest.java
@@ -0,0 +1,84 @@
+// 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.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Ordering;
+
+import org.junit.Test;
+
+import java.util.List;
+
+public class RegexListSearcherTest {
+  private static final List<String> EMPTY = ImmutableList.of();
+
+  @Test
+  public void emptyList() {
+    assertSearchReturns(EMPTY, "pat", EMPTY);
+  }
+
+  @Test
+  public void hasMatch() {
+    List<String> list = ImmutableList.of("bar", "foo", "quux");
+    assertTrue(RegexListSearcher.ofStrings("foo").hasMatch(list));
+    assertFalse(RegexListSearcher.ofStrings("xyz").hasMatch(list));
+  }
+
+  @Test
+  public void anchors() {
+    List<String> list = ImmutableList.of("foo");
+    assertSearchReturns(list, "^f.*", list);
+    assertSearchReturns(list, "^f.*o$", list);
+    assertSearchReturns(list, "f.*o$", list);
+    assertSearchReturns(list, "f.*o$", list);
+    assertSearchReturns(EMPTY, "^.*\\$", list);
+  }
+
+  @Test
+  public void noCommonPrefix() {
+    List<String> list = ImmutableList.of("bar", "foo", "quux");
+    assertSearchReturns(ImmutableList.of("foo"), "f.*", list);
+    assertSearchReturns(ImmutableList.of("foo"), ".*o.*", list);
+    assertSearchReturns(ImmutableList.of("bar", "foo", "quux"), ".*[aou].*",
+        list);
+  }
+
+  @Test
+  public void commonPrefix() {
+    List<String> list = ImmutableList.of(
+        "bar",
+        "baz",
+        "foo1",
+        "foo2",
+        "foo3",
+        "quux");
+    assertSearchReturns(ImmutableList.of("bar", "baz"), "b.*", list);
+    assertSearchReturns(ImmutableList.of("foo1", "foo2"), "foo[12]", list);
+    assertSearchReturns(ImmutableList.of("foo1", "foo2", "foo3"), "foo.*",
+        list);
+    assertSearchReturns(ImmutableList.of("quux"), "q.*", list);
+  }
+
+  private void assertSearchReturns(List<?> expected, String re,
+    List<String> inputs) {
+    assertTrue(Ordering.natural().isOrdered(inputs));
+    assertEquals(expected,
+        ImmutableList.copyOf(RegexListSearcher.ofStrings(re).search(inputs)));
+  }
+}