Merge changes Ied06561c,Idff421e1,Iaf12bab5,I2b744f5a

* changes:
  Add permission_sort cache to remember sort orderings
  Reuse cached RefControl data in FunctionState
  Refactor how permissions are matched by ProjectControl, RefControl
  Cache effective capabilities to improve lookup performance
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 9bbc5e0..d17109d 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -432,6 +432,13 @@
 cache automatically updates when a user first creates their account
 within Gerrit, so the cache expire time is largely irrelevant.
 
+cache `"permission_sort"`::
++
+Caches the order access control sections must be applied to a
+reference.  Sorting the sections can be expensive when regular
+expressions are used, so this cache remembers the ordering for
+each branch.
+
 cache `"projects"`::
 +
 Caches the project description records, from the `projects` table
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
index c274702..dedc656 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
@@ -154,8 +154,7 @@
         db.patchSetApprovals().byChange(changeId).toList();
 
     if (detail.getChange().getStatus().isOpen()) {
-      final FunctionState fs =
-          functionState.create(detail.getChange(), psId, allApprovals);
+      final FunctionState fs = functionState.create(control, psId, allApprovals);
 
       for (final ApprovalType at : approvalTypes.getApprovalTypes()) {
         CategoryFunction.forCategory(at.getCategory()).run(at, fs);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
index 394b43f..59755e3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
@@ -182,7 +182,7 @@
             final Map<ApprovalCategory.Id, PatchSetApproval> psas =
                 new HashMap<ApprovalCategory.Id, PatchSetApproval>();
             final FunctionState fs =
-                functionStateFactory.create(change, ps_id, psas.values());
+                functionStateFactory.create(cc, ps_id, psas.values());
 
             for (final PatchSetApproval ca : db.patchSetApprovals()
                 .byPatchSetUser(ps_id, aid)) {
@@ -229,7 +229,7 @@
             final Map<ApprovalCategory.Id, PatchSetApproval> psas =
                 new HashMap<ApprovalCategory.Id, PatchSetApproval>();
             final FunctionState fs =
-                functionStateFactory.create(change, ps_id, psas.values());
+                functionStateFactory.create(cc, ps_id, psas.values());
 
             for (PatchSetApproval ca : db.patchSetApprovals().byPatchSet(ps_id)) {
               final ApprovalCategory.Id category = ca.getCategoryId();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
new file mode 100644
index 0000000..3d03ed8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
@@ -0,0 +1,108 @@
+// Copyright (C) 2011 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.account;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.AccountGroup;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Caches active {@link GlobalCapability} set for a site. */
+public class CapabilityCollection {
+  private final Map<String, List<PermissionRule>> permissions;
+
+  public final List<PermissionRule> administrateServer;
+  public final List<PermissionRule> priority;
+  public final List<PermissionRule> queryLimit;
+
+  public CapabilityCollection(AccessSection section) {
+    if (section == null) {
+      section = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
+    }
+
+    Map<String, List<PermissionRule>> tmp =
+        new HashMap<String, List<PermissionRule>>();
+    for (Permission permission : section.getPermissions()) {
+      for (PermissionRule rule : permission.getRules()) {
+        if (rule.getAction() != PermissionRule.Action.DENY) {
+          List<PermissionRule> r = tmp.get(permission.getName());
+          if (r == null) {
+            r = new ArrayList<PermissionRule>(2);
+            tmp.put(permission.getName(), r);
+          }
+          r.add(rule);
+        }
+      }
+    }
+    configureDefaults(tmp, section);
+
+    Map<String, List<PermissionRule>> res =
+        new HashMap<String, List<PermissionRule>>();
+    for (Map.Entry<String, List<PermissionRule>> e : tmp.entrySet()) {
+      List<PermissionRule> rules = e.getValue();
+      if (rules.size() == 1) {
+        res.put(e.getKey(), Collections.singletonList(rules.get(0)));
+      } else {
+        res.put(e.getKey(), Collections.unmodifiableList(
+            Arrays.asList(rules.toArray(new PermissionRule[rules.size()]))));
+      }
+    }
+    permissions = Collections.unmodifiableMap(res);
+
+    administrateServer = getPermission(GlobalCapability.ADMINISTRATE_SERVER);
+    priority = getPermission(GlobalCapability.PRIORITY);
+    queryLimit = getPermission(GlobalCapability.QUERY_LIMIT);
+  }
+
+  public List<PermissionRule> getPermission(String permissionName) {
+    List<PermissionRule> r = permissions.get(permissionName);
+    return r != null ? r : Collections.<PermissionRule> emptyList();
+  }
+
+  private static final GroupReference anonymous = new GroupReference(
+      AccountGroup.ANONYMOUS_USERS,
+      "Anonymous Users");
+
+  private static void configureDefaults(Map<String, List<PermissionRule>> out,
+      AccessSection section) {
+    configureDefault(out, section, GlobalCapability.QUERY_LIMIT, anonymous);
+  }
+
+  private static void configureDefault(Map<String, List<PermissionRule>> out,
+      AccessSection section, String capName, GroupReference group) {
+    if (doesNotDeclare(section, capName)) {
+      PermissionRange.WithDefaults range = GlobalCapability.getRange(capName);
+      if (range != null) {
+        PermissionRule rule = new PermissionRule(group);
+        rule.setRange(range.getDefaultMin(), range.getDefaultMax());
+        out.put(capName, Collections.singletonList(rule));
+      }
+    }
+  }
+
+  private static boolean doesNotDeclare(AccessSection section, String capName) {
+    return section.getPermission(capName) == null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index 39719ad..57d01fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -14,10 +14,8 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.AccountGroup;
@@ -25,7 +23,6 @@
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -42,16 +39,17 @@
     public CapabilityControl create(CurrentUser user);
   }
 
-  private final ProjectState state;
+  private final CapabilityCollection capabilities;
   private final CurrentUser user;
-  private Map<String, List<PermissionRule>> permissions;
+  private final Map<String, List<PermissionRule>> effective;
 
   private Boolean canAdministrateServer;
 
   @Inject
   CapabilityControl(ProjectCache projectCache, @Assisted CurrentUser currentUser) {
-    state = projectCache.getAllProjects();
+    capabilities = projectCache.getAllProjects().getCapabilityCollection();
     user = currentUser;
+    effective = new HashMap<String, List<PermissionRule>>();
   }
 
   /** Identity of the user the control will compute for. */
@@ -63,7 +61,7 @@
   public boolean canAdministrateServer() {
     if (canAdministrateServer == null) {
       canAdministrateServer = user instanceof PeerDaemonUser
-          || canPerform(GlobalCapability.ADMINISTRATE_SERVER);
+          || matchAny(capabilities.administrateServer);
     }
     return canAdministrateServer;
   }
@@ -131,19 +129,21 @@
     // the 'CI Servers' actually use the BATCH queue while everyone else gets
     // to use the INTERACTIVE queue without additional grants.
     //
-    List<PermissionRule> rules = access(GlobalCapability.PRIORITY);
+    Set<AccountGroup.UUID> groups = user.getEffectiveGroups();
     boolean batch = false;
-    for (PermissionRule r : rules) {
-      switch (r.getAction()) {
-        case INTERACTIVE:
-          if (!isGenericGroup(r.getGroup())) {
-            return QueueProvider.QueueType.INTERACTIVE;
-          }
-          break;
+    for (PermissionRule r : capabilities.priority) {
+      if (match(groups, r)) {
+        switch (r.getAction()) {
+          case INTERACTIVE:
+            if (!isGenericGroup(r.getGroup())) {
+              return QueueProvider.QueueType.INTERACTIVE;
+            }
+            break;
 
-        case BATCH:
-          batch = true;
-          break;
+          case BATCH:
+            batch = true;
+            break;
+        }
       }
     }
 
@@ -186,71 +186,53 @@
 
   /** Rules for the given permission, or the empty list. */
   private List<PermissionRule> access(String permissionName) {
-    List<PermissionRule> r = permissions().get(permissionName);
-    return r != null ? r : Collections.<PermissionRule> emptyList();
-  }
-
-  /** All rules that pertain to this user. */
-  private Map<String, List<PermissionRule>> permissions() {
-    if (permissions == null) {
-      permissions = indexPermissions();
-    }
-    return permissions;
-  }
-
-  private Map<String, List<PermissionRule>> indexPermissions() {
-    Map<String, List<PermissionRule>> res =
-        new HashMap<String, List<PermissionRule>>();
-
-    AccessSection section = state.getConfig()
-      .getAccessSection(AccessSection.GLOBAL_CAPABILITIES);
-    if (section == null) {
-      section = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
+    List<PermissionRule> rules = effective.get(permissionName);
+    if (rules != null) {
+      return rules;
     }
 
-    for (Permission permission : section.getPermissions()) {
-      for (PermissionRule rule : permission.getRules()) {
-        if (matchGroup(rule.getGroup().getUUID())) {
-          if (rule.getAction() != PermissionRule.Action.DENY) {
-            List<PermissionRule> r = res.get(permission.getName());
-            if (r == null) {
-              r = new ArrayList<PermissionRule>(2);
-              res.put(permission.getName(), r);
-            }
-            r.add(rule);
-          }
-        }
+    rules = capabilities.getPermission(permissionName);
+
+    if (rules.isEmpty()) {
+      effective.put(permissionName, rules);
+      return rules;
+    }
+
+    Set<AccountGroup.UUID> groups = user.getEffectiveGroups();
+    if (rules.size() == 1) {
+      if (!match(groups, rules.get(0))) {
+        rules = Collections.emptyList();
+      }
+      effective.put(permissionName, rules);
+      return rules;
+    }
+
+    List<PermissionRule> mine = new ArrayList<PermissionRule>(rules.size());
+    for (PermissionRule rule : rules) {
+      if (match(groups, rule)) {
+        mine.add(rule);
       }
     }
 
-    configureDefaults(res, section);
-    return res;
+    if (mine.isEmpty()) {
+      mine = Collections.emptyList();
+    }
+    effective.put(permissionName, mine);
+    return mine;
   }
 
-  private boolean matchGroup(AccountGroup.UUID uuid) {
-    Set<AccountGroup.UUID> userGroups = getCurrentUser().getEffectiveGroups();
-    return userGroups.contains(uuid);
-  }
-
-  private static final GroupReference anonymous = new GroupReference(
-      AccountGroup.ANONYMOUS_USERS,
-      "Anonymous Users");
-
-  private static void configureDefaults(
-      Map<String, List<PermissionRule>> res,
-      AccessSection section) {
-    configureDefault(res, section, GlobalCapability.QUERY_LIMIT, anonymous);
-  }
-
-  private static void configureDefault(Map<String, List<PermissionRule>> res,
-      AccessSection section, String capName, GroupReference group) {
-    if (section.getPermission(capName) == null) {
-      PermissionRange.WithDefaults range = GlobalCapability.getRange(capName);
-      if (range != null) {
-        PermissionRule rule = new PermissionRule(group);
-        rule.setRange(range.getDefaultMin(), range.getDefaultMax());
-        res.put(capName, Collections.singletonList(rule));
+  private boolean matchAny(List<PermissionRule> rules) {
+    Set<AccountGroup.UUID> groups = user.getEffectiveGroups();
+    for (PermissionRule rule : rules) {
+      if (match(groups, rule)) {
+        return true;
       }
     }
+    return false;
+  }
+
+  private static boolean match(Set<AccountGroup.UUID> groups,
+      PermissionRule rule) {
+    return groups.contains(rule.getGroup().getUUID());
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java
new file mode 100644
index 0000000..268fe2b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2011 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.git;
+
+package com.google.gerrit.server.cache;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An infinitely sized cache backed by java.util.ConcurrentHashMap.
+ * <p>
+ * This cache type is only suitable for unit tests, as it has no upper limit on
+ * number of items held in the cache. No upper limit can result in memory leaks
+ * in production servers.
+ */
+public class ConcurrentHashMapCache<K, V> implements Cache<K, V> {
+  private final ConcurrentHashMap<K, V> map = new ConcurrentHashMap<K, V>();
+
+  @Override
+  public V get(K key) {
+    return map.get(key);
+  }
+
+  @Override
+  public void put(K key, V value) {
+    map.put(key, value);
+  }
+
+  @Override
+  public void remove(K key) {
+    map.remove(key);
+  }
+
+  @Override
+  public void removeAll() {
+    map.clear();
+  }
+
+  @Override
+  public long getTimeToLive(TimeUnit unit) {
+    return 0;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index a564e56..7bca406 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -58,10 +58,11 @@
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.AccessControlModule;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.PermissionCollection;
 import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.project.SectionSortCache;
 import com.google.gerrit.server.tools.ToolsCatalog;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.server.workflow.FunctionState;
@@ -147,6 +148,7 @@
     install(GroupIncludeCacheImpl.module());
     install(PatchListCacheImpl.module());
     install(ProjectCacheImpl.module());
+    install(SectionSortCache.module());
     install(TagCache.module());
     install(new AccessControlModule());
     install(new GitModule());
@@ -156,7 +158,7 @@
     factory(CapabilityControl.Factory.class);
     factory(GroupInfoCacheFactory.Factory.class);
     factory(ProjectState.Factory.class);
-    factory(RefControl.Factory.class);
+    bind(PermissionCollection.Factory.class);
 
     bind(FileTypeRegistry.class).to(MimeUtilFileTypeRegistry.class);
     bind(WorkQueue.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index a050cfd..4c386e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
@@ -40,6 +40,7 @@
 import com.google.gerrit.server.patch.PublishComments;
 import com.google.gerrit.server.patch.RemoveReviewer;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.PerRequestProjectControlCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeQueryRewriter;
@@ -58,6 +59,7 @@
     bind(ChangeQueryRewriter.class);
 
     bind(AnonymousUser.class).in(RequestScoped.class);
+    bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
     bind(ChangeControl.Factory.class).in(SINGLETON);
     bind(GroupControl.Factory.class).in(SINGLETON);
     bind(ProjectControl.Factory.class).in(SINGLETON);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 88676bd..2c756d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
@@ -40,6 +40,8 @@
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.workflow.CategoryFunction;
@@ -137,6 +139,7 @@
   private final ApprovalTypes approvalTypes;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final ChangeControl.GenericFactory changeControlFactory;
   private final MergeQueue mergeQueue;
 
   private final PersonIdent myIdent;
@@ -167,6 +170,7 @@
       @CanonicalWebUrl @Nullable final Provider<String> cwu,
       final ApprovalTypes approvalTypes, final PatchSetInfoFactory psif,
       final IdentifiedUser.GenericFactory iuf,
+      final ChangeControl.GenericFactory changeControlFactory,
       @GerritPersonIdent final PersonIdent myIdent,
       final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch,
       final ChangeHookRunner hooks, final AccountCache accountCache,
@@ -183,6 +187,7 @@
     this.approvalTypes = approvalTypes;
     patchSetInfoFactory = psif;
     identifiedUserFactory = iuf;
+    this.changeControlFactory = changeControlFactory;
     this.mergeQueue = mergeQueue;
     this.hooks = hooks;
     this.accountCache = accountCache;
@@ -1240,7 +1245,11 @@
       c.setStatus(Change.Status.MERGED);
       final List<PatchSetApproval> approvals =
           schema.patchSetApprovals().byChange(changeId).toList();
-      final FunctionState fs = functionState.create(c, merged, approvals);
+      final FunctionState fs = functionState.create(
+          changeControlFactory.controlFor(
+              c,
+              identifiedUserFactory.create(c.getOwner())),
+              merged, approvals);
       for (ApprovalType at : approvalTypes.getApprovalTypes()) {
         CategoryFunction.forCategory(at.getCategory()).run(at, fs);
       }
@@ -1256,6 +1265,8 @@
         a.cache(c);
       }
       schema.patchSetApprovals().update(approvals);
+    } catch (NoSuchChangeException err) {
+      log.warn("Cannot normalize approvals for change " + changeId, err);
     } catch (OrmException err) {
       log.warn("Cannot normalize approvals for change " + changeId, err);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
index 244648a..8efb4ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
@@ -120,7 +120,7 @@
 
     final boolean isCurrent = patchSetId.equals(change.currentPatchSetId());
     if (isCurrent && change.getStatus().isOpen()) {
-      publishApprovals();
+      publishApprovals(ctl);
     } else if (! approvals.isEmpty()) {
       throw new InvalidChangeOperationException("Change is closed");
     } else {
@@ -141,7 +141,7 @@
     db.patchComments().update(drafts);
   }
 
-  private void publishApprovals() throws OrmException {
+  private void publishApprovals(ChangeControl ctl) throws OrmException {
     ChangeUtil.updated(change);
 
     final Set<ApprovalCategory.Id> dirty = new HashSet<ApprovalCategory.Id>();
@@ -169,7 +169,7 @@
     // Normalize all of the items the user is changing.
     //
     final FunctionState functionState =
-        functionStateFactory.create(change, patchSetId, all);
+        functionStateFactory.create(ctl, patchSetId, all);
     for (final ApprovalCategoryValue.Id want : approvals) {
       final PatchSetApproval a = mine.get(want.getParentKey());
       final short o = a.getValue();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java
new file mode 100644
index 0000000..500ac06
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2011 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.project;
+
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.servlet.RequestScoped;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Caches {@link ProjectControl} objects for the current user of the request. */
+@RequestScoped
+public class PerRequestProjectControlCache {
+  private final ProjectCache projectCache;
+  private final CurrentUser user;
+  private final Map<Project.NameKey, ProjectControl> controls;
+
+  @Inject
+  PerRequestProjectControlCache(ProjectCache projectCache,
+      CurrentUser userProvider) {
+    this.projectCache = projectCache;
+    this.user = userProvider;
+    this.controls = new HashMap<Project.NameKey, ProjectControl>();
+  }
+
+  ProjectControl get(Project.NameKey nameKey) throws NoSuchProjectException {
+    ProjectControl ctl = controls.get(nameKey);
+    if (ctl == null) {
+      ProjectState p = projectCache.get(nameKey);
+      if (p == null) {
+        throw new NoSuchProjectException(nameKey);
+      }
+      ctl = p.controlFor(user);
+      controls.put(nameKey, ctl);
+    }
+    return ctl;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
new file mode 100644
index 0000000..ca3bb4d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
@@ -0,0 +1,218 @@
+// Copyright (C) 2011 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.project;
+
+import static com.google.gerrit.server.project.RefControl.isRE;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Effective permissions applied to a reference in a project.
+ * <p>
+ * A collection may be user specific if a matching {@link AccessSection} uses
+ * "${username}" in its name. The permissions granted in that section may only
+ * be granted to the username that appears in the reference name, and also only
+ * if the user is a member of the relevant group.
+ */
+public class PermissionCollection {
+  @Singleton
+  public static class Factory {
+    private final SectionSortCache sorter;
+
+    @Inject
+    Factory(SectionSortCache sorter) {
+      this.sorter = sorter;
+    }
+
+    /**
+     * Get all permissions that apply to a reference.
+     *
+     * @param matcherList collection of sections that should be considered, in
+     *        priority order (project specific definitions must appear before
+     *        inherited ones).
+     * @param ref reference being accessed.
+     * @param username if the reference is a per-user reference, access sections
+     *        using the parameter variable "${username}" will first have {@code
+     *        username} inserted into them before seeing if they apply to the
+     *        reference named by {@code ref}. If null, per-user references are
+     *        ignored.
+     * @return map of permissions that apply to this reference, keyed by
+     *         permission name.
+     */
+    PermissionCollection filter(Iterable<SectionMatcher> matcherList,
+        String ref, String username) {
+      if (isRE(ref)) {
+        ref = RefControl.shortestExample(ref);
+      } else if (ref.endsWith("/*")) {
+        ref = ref.substring(0, ref.length() - 1);
+      }
+
+      boolean perUser = false;
+      List<AccessSection> sections = new ArrayList<AccessSection>();
+      for (SectionMatcher matcher : matcherList) {
+        // If the matcher has to expand parameters and its prefix matches the
+        // reference there is a very good chance the reference is actually user
+        // specific, even if the matcher does not match the reference. Since its
+        // difficult to prove this is true all of the time, use an approximation
+        // to prevent reuse of collections across users accessing the same
+        // reference at the same time.
+        //
+        // This check usually gets caching right, as most per-user references
+        // use a common prefix like "refs/sandbox/" or "refs/heads/users/"
+        // that will never be shared with non-user references, and the per-user
+        // references are usually less frequent than the non-user references.
+        //
+        if (username != null && !perUser
+            && matcher instanceof SectionMatcher.ExpandParameters) {
+          perUser = ((SectionMatcher.ExpandParameters) matcher).matchPrefix(ref);
+        }
+
+        if (matcher.match(ref, username)) {
+          sections.add(matcher.section);
+        }
+      }
+      sorter.sort(ref, sections);
+
+      Set<SeenRule> seen = new HashSet<SeenRule>();
+      Set<String> exclusiveGroupPermissions = new HashSet<String>();
+
+      HashMap<String, List<PermissionRule>> permissions =
+          new HashMap<String, List<PermissionRule>>();
+      for (AccessSection section : sections) {
+        for (Permission permission : section.getPermissions()) {
+          if (exclusiveGroupPermissions.contains(permission.getName())) {
+            continue;
+          }
+
+          for (PermissionRule rule : permission.getRules()) {
+            SeenRule s = new SeenRule(section, permission, rule);
+            if (seen.add(s) && !rule.getDeny()) {
+              List<PermissionRule> r = permissions.get(permission.getName());
+              if (r == null) {
+                r = new ArrayList<PermissionRule>(2);
+                permissions.put(permission.getName(), r);
+              }
+              r.add(rule);
+            }
+          }
+
+          if (permission.getExclusiveGroup()) {
+            exclusiveGroupPermissions.add(permission.getName());
+          }
+        }
+      }
+
+      return new PermissionCollection(ref, permissions, perUser ? username : null);
+    }
+  }
+
+  private final String ref;
+  private final Map<String, List<PermissionRule>> rules;
+  private final String username;
+
+  private PermissionCollection(String ref,
+      Map<String, List<PermissionRule>> rules, String username) {
+    this.ref = ref;
+    this.rules = rules;
+    this.username = username;
+  }
+
+  /**
+   * @return true if a "${username}" pattern might need to be expanded to build
+   *         this collection, making the results user specific.
+   */
+  public boolean isUserSpecific() {
+    return username != null;
+  }
+
+  /**
+   * Obtain all permission rules for a given type of permission.
+   *
+   * @param permissionName type of permission.
+   * @return all rules that apply to this reference, for any group. Never null;
+   *         the empty list is returned when there are no rules for the requested
+   *         permission name.
+   */
+  public List<PermissionRule> getPermission(String permissionName) {
+    List<PermissionRule> r = rules.get(permissionName);
+    return r != null ? r : Collections.<PermissionRule> emptyList();
+  }
+
+  /**
+   * Obtain all declared permission rules that match the reference.
+   *
+   * @return all rules. The collection will iterate a permission if it was
+   *         declared in the project configuration, either directly or
+   *         inherited. If the project owner did not use a known permission (for
+   *         example {@link Permission#FORGE_SERVER}, then it will not be
+   *         represented in the result even if {@link #getPermission(String)}
+   *         returns an empty list for the same permission.
+   */
+  public Iterable<Map.Entry<String, List<PermissionRule>>> getDeclaredPermissions() {
+    return rules.entrySet();
+  }
+
+  /** Tracks whether or not a permission has been overridden. */
+  private static class SeenRule {
+    final String refPattern;
+    final String permissionName;
+    final AccountGroup.UUID group;
+
+    SeenRule(AccessSection section, Permission permission, PermissionRule rule) {
+      refPattern = section.getName();
+      permissionName = permission.getName();
+      group = rule.getGroup().getUUID();
+    }
+
+    @Override
+    public int hashCode() {
+      int hc = refPattern.hashCode();
+      hc = hc * 31 + permissionName.hashCode();
+      if (group != null) {
+        hc = hc * 31 + group.hashCode();
+      }
+      return hc;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (other instanceof SeenRule) {
+        SeenRule a = this;
+        SeenRule b = (SeenRule) other;
+        return a.refPattern.equals(b.refPattern) //
+            && a.permissionName.equals(b.permissionName) //
+            && eq(a.group, b.group);
+      }
+      return false;
+    }
+
+    private boolean eq(AccountGroup.UUID a, AccountGroup.UUID b) {
+      return a != null && b != null && a.equals(b);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index e9247af..8a2966a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.project;
 
-import static com.google.gerrit.common.CollectionsUtil.isAnyIncludedIn;
-
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Capable;
@@ -46,10 +44,11 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 import javax.annotation.Nullable;
@@ -81,22 +80,16 @@
   }
 
   public static class Factory {
-    private final ProjectCache projectCache;
-    private final Provider<CurrentUser> user;
+    private final Provider<PerRequestProjectControlCache> userCache;
 
     @Inject
-    Factory(final ProjectCache pc, final Provider<CurrentUser> cu) {
-      projectCache = pc;
-      user = cu;
+    Factory(Provider<PerRequestProjectControlCache> uc) {
+      userCache = uc;
     }
 
     public ProjectControl controlFor(final Project.NameKey nameKey)
         throws NoSuchProjectException {
-      final ProjectState p = projectCache.get(nameKey);
-      if (p == null) {
-        throw new NoSuchProjectException(nameKey);
-      }
-      return p.controlFor(user.get());
+      return userCache.get().get(nameKey);
     }
 
     public ProjectControl validateFor(final Project.NameKey nameKey)
@@ -130,34 +123,38 @@
   private final Set<AccountGroup.UUID> receiveGroups;
 
   private final String canonicalWebUrl;
-  private final RefControl.Factory refControlFactory;
   private final SchemaFactory<ReviewDb> schema;
   private final CurrentUser user;
   private final ProjectState state;
   private final GroupCache groupCache;
+  private final PermissionCollection.Factory permissionFilter;
 
-
-  private Collection<AccessSection> access;
+  private List<SectionMatcher> allSections;
+  private Map<String, RefControl> refControls;
+  private Boolean declaredOwner;
 
   @Inject
   ProjectControl(@GitUploadPackGroups Set<AccountGroup.UUID> uploadGroups,
       @GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
       final SchemaFactory<ReviewDb> schema, final GroupCache groupCache,
+      final PermissionCollection.Factory permissionFilter,
       @CanonicalWebUrl @Nullable final String canonicalWebUrl,
-      final RefControl.Factory refControlFactory,
       @Assisted CurrentUser who, @Assisted ProjectState ps) {
     this.uploadGroups = uploadGroups;
     this.receiveGroups = receiveGroups;
     this.schema = schema;
     this.groupCache = groupCache;
+    this.permissionFilter = permissionFilter;
     this.canonicalWebUrl = canonicalWebUrl;
-    this.refControlFactory = refControlFactory;
     user = who;
     state = ps;
   }
 
-  public ProjectControl forUser(final CurrentUser who) {
-    return state.controlFor(who);
+  public ProjectControl forUser(CurrentUser who) {
+    ProjectControl r = state.controlFor(who);
+    // Not per-user, and reusing saves lookup time.
+    r.allSections = allSections;
+    return r;
   }
 
   public ChangeControl controlFor(final Change change) {
@@ -169,7 +166,17 @@
   }
 
   public RefControl controlForRef(String refName) {
-    return refControlFactory.create(this, refName);
+    if (refControls == null) {
+      refControls = new HashMap<String, RefControl>();
+    }
+    RefControl ctl = refControls.get(refName);
+    if (ctl == null) {
+      PermissionCollection relevant =
+          permissionFilter.filter(access(), refName, user.getUserName());
+      ctl = new RefControl(this, refName, relevant);
+      refControls.put(refName, ctl);
+    }
+    return ctl;
   }
 
   public CurrentUser getCurrentUser() {
@@ -181,7 +188,7 @@
   }
 
   public Project getProject() {
-    return getProjectState().getProject();
+    return state.getProject();
   }
 
   /** Can this user see this project exists? */
@@ -203,20 +210,27 @@
 
   /** Is this project completely visible for replication? */
   boolean visibleForReplication() {
-    return getCurrentUser() instanceof ReplicationUser
-        && ((ReplicationUser) getCurrentUser()).isEverythingVisible();
+    return user instanceof ReplicationUser
+        && ((ReplicationUser) user).isEverythingVisible();
   }
 
   /** Is this user a project owner? Ownership does not imply {@link #isVisible()} */
   public boolean isOwner() {
-    return controlForRef(AccessSection.ALL).isOwner()
-        || getCurrentUser().getCapabilities().canAdministrateServer();
+    return isDeclaredOwner()
+      || user.getCapabilities().canAdministrateServer();
+  }
+
+  private boolean isDeclaredOwner() {
+    if (declaredOwner == null) {
+      declaredOwner = state.isOwner(user.getEffectiveGroups());
+    }
+    return declaredOwner;
   }
 
   /** Does this user have ownership on at least one reference name? */
   public boolean isOwnerAnyRef() {
     return canPerformOnAnyRef(Permission.OWNER)
-        || getCurrentUser().getCapabilities().canAdministrateServer();
+        || user.getCapabilities().canAdministrateServer();
   }
 
   /** @return true if the user can upload to at least one reference */
@@ -370,33 +384,16 @@
     return value == null || value.trim().equals("");
   }
 
-  /**
-   * @return the effective groups of the current user for this project
-   */
-  private Set<AccountGroup.UUID> getEffectiveUserGroups() {
-    final Set<AccountGroup.UUID> userGroups = user.getEffectiveGroups();
-    if (isOwner()) {
-      final Set<AccountGroup.UUID> userGroupsOnProject =
-          new HashSet<AccountGroup.UUID>(userGroups.size() + 1);
-      userGroupsOnProject.addAll(userGroups);
-      userGroupsOnProject.add(AccountGroup.PROJECT_OWNERS);
-      return Collections.unmodifiableSet(userGroupsOnProject);
-    } else {
-      return userGroups;
-    }
-  }
-
   private boolean canPerformOnAnyRef(String permissionName) {
-    final Set<AccountGroup.UUID> groups = getEffectiveUserGroups();
-
-    for (AccessSection section : access()) {
+    for (SectionMatcher matcher : access()) {
+      AccessSection section = matcher.section;
       Permission permission = section.getPermission(permissionName);
       if (permission == null) {
         continue;
       }
 
       for (PermissionRule rule : permission.getRules()) {
-        if (rule.getDeny()) {
+        if (rule.getDeny() || !match(rule)) {
           continue;
         }
 
@@ -404,9 +401,10 @@
         // approximation.  There might be overrides and doNotInherit
         // that would render this to be false.
         //
-        if (groups.contains(rule.getGroup().getUUID())
-            && controlForRef(section.getName()).canPerform(permissionName)) {
+        if (controlForRef(section.getName()).canPerform(permissionName)) {
           return true;
+        } else {
+          break;
         }
       }
     }
@@ -435,7 +433,8 @@
 
   private Set<String> allRefPatterns(String permissionName) {
     Set<String> all = new HashSet<String>();
-    for (AccessSection section : access()) {
+    for (SectionMatcher matcher : access()) {
+      AccessSection section = matcher.section;
       Permission permission = section.getPermission(permissionName);
       if (permission != null) {
         all.add(section.getName());
@@ -444,18 +443,40 @@
     return all;
   }
 
-  Collection<AccessSection> access() {
-    if (access == null) {
-      access = state.getAllAccessSections();
+  private List<SectionMatcher> access() {
+    if (allSections == null) {
+      allSections = state.getAllSections();
     }
-    return access;
+    return allSections;
+  }
+
+  boolean match(PermissionRule rule) {
+    return match(rule.getGroup().getUUID());
+  }
+
+  boolean match(AccountGroup.UUID uuid) {
+    if (AccountGroup.PROJECT_OWNERS.equals(uuid)) {
+      return isDeclaredOwner();
+    } else {
+      return user.getEffectiveGroups().contains(uuid);
+    }
   }
 
   public boolean canRunUploadPack() {
-    return isAnyIncludedIn(uploadGroups, getEffectiveUserGroups());
+    for (AccountGroup.UUID group : uploadGroups) {
+      if (match(group)) {
+        return true;
+      }
+    }
+    return false;
   }
 
   public boolean canRunReceivePack() {
-    return isAnyIncludedIn(receiveGroups, getEffectiveUserGroups());
+    for (AccountGroup.UUID group : receiveGroups) {
+      if (match(group)) {
+        return true;
+      }
+    }
+    return false;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 376d1a7..c587829 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.common.CollectionsUtil;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
@@ -23,6 +24,7 @@
 import com.google.gerrit.rules.PrologEnvironment;
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityCollection;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
@@ -65,6 +67,11 @@
   /** Last system time the configuration's revision was examined. */
   private volatile long lastCheckTime;
 
+  /** Local access sections, wrapped in SectionMatchers for faster evaluation. */
+  private volatile List<SectionMatcher> localAccessSections;
+
+  /** If this is all projects, the capabilities used by the server. */
+  private final CapabilityCollection capabilities;
 
   @Inject
   protected ProjectState(
@@ -82,6 +89,9 @@
     this.gitMgr = gitMgr;
     this.rulesCache = rulesCache;
     this.config = config;
+    this.capabilities = isAllProjects
+      ? new CapabilityCollection(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
+      : null;
 
     HashSet<AccountGroup.UUID> groups = new HashSet<AccountGroup.UUID>();
     AccessSection all = config.getAccessSection(AccessSection.ALL);
@@ -127,64 +137,77 @@
     }
   }
 
+  /**
+   * @return cached computation of all global capabilities. This should only be
+   *         invoked on the state from {@link ProjectCache#getAllProjects()}.
+   *         Null on any other project.
+   */
+  public CapabilityCollection getCapabilityCollection() {
+    return capabilities;
+  }
+
   /** @return Construct a new PrologEnvironment for the calling thread. */
   public PrologEnvironment newPrologEnvironment() throws CompileException {
     PrologMachineCopy pmc = rulesMachine;
     if (pmc == null) {
       pmc = rulesCache.loadMachine(
           getProject().getNameKey(),
-          getConfig().getRulesId());
+          config.getRulesId());
       rulesMachine = pmc;
     }
     return envFactory.create(pmc);
   }
 
   public Project getProject() {
-    return getConfig().getProject();
+    return config.getProject();
   }
 
   public ProjectConfig getConfig() {
     return config;
   }
 
-  /** Get the rights that pertain only to this project. */
-  public Collection<AccessSection> getLocalAccessSections() {
-    return getConfig().getAccessSections();
+  /** Get the sections that pertain only to this project. */
+  private List<SectionMatcher> getLocalAccessSections() {
+    List<SectionMatcher> sm = localAccessSections;
+    if (sm == null) {
+      Collection<AccessSection> fromConfig = config.getAccessSections();
+      sm = new ArrayList<SectionMatcher>(fromConfig.size());
+      for (AccessSection section : fromConfig) {
+        SectionMatcher matcher = SectionMatcher.wrap(section);
+        if (matcher != null) {
+          sm.add(matcher);
+        }
+      }
+      localAccessSections = sm;
+    }
+    return sm;
   }
 
-  /** Get the rights this project inherits. */
-  public Collection<AccessSection> getInheritedAccessSections() {
+  /**
+   * Obtain all local and inherited sections. This collection is looked up
+   * dynamically and is not cached. Callers should try to cache this result
+   * per-request as much as possible.
+   */
+  List<SectionMatcher> getAllSections() {
     if (isAllProjects) {
-      return Collections.emptyList();
+      return getLocalAccessSections();
     }
 
-    List<AccessSection> inherited = new ArrayList<AccessSection>();
+    List<SectionMatcher> all = new ArrayList<SectionMatcher>();
     Set<Project.NameKey> seen = new HashSet<Project.NameKey>();
-    Project.NameKey parent = getProject().getParent();
+    seen.add(getProject().getNameKey());
 
-    while (parent != null && seen.add(parent)) {
-      ProjectState s = projectCache.get(parent);
-      if (s != null) {
-        inherited.addAll(s.getLocalAccessSections());
-        parent = s.getProject().getParent();
-      } else {
+    ProjectState s = this;
+    do {
+      all.addAll(s.getLocalAccessSections());
+
+      Project.NameKey parent = s.getProject().getParent();
+      if (parent == null || !seen.add(parent)) {
         break;
       }
-    }
-
-    // The root of the tree is the special "All-Projects" case.
-    if (parent == null) {
-      inherited.addAll(projectCache.getAllProjects().getLocalAccessSections());
-    }
-
-    return inherited;
-  }
-
-  /** Get both local and inherited access sections. */
-  public Collection<AccessSection> getAllAccessSections() {
-    List<AccessSection> all = new ArrayList<AccessSection>();
-    all.addAll(getLocalAccessSections());
-    all.addAll(getInheritedAccessSections());
+      s = projectCache.get(parent);
+    } while (s != null);
+    all.addAll(projectCache.getAllProjects().getLocalAccessSections());
     return all;
   }
 
@@ -209,30 +232,26 @@
   }
 
   /**
-   * @return all {@link AccountGroup}'s that are allowed to administrate the
-   *         complete project. This includes all groups to which the owner
-   *         privilege for 'refs/*' is assigned for this project (the local
-   *         owners) and all groups to which the owner privilege for 'refs/*' is
-   *         assigned for one of the parent projects (the inherited owners).
+   * @return true if any of the groups listed in {@code groups} was declared to
+   *         be an owner of this project, or one of its parent projects..
    */
-  public Set<AccountGroup.UUID> getAllOwners() {
-    HashSet<AccountGroup.UUID> owners = new HashSet<AccountGroup.UUID>();
-    owners.addAll(localOwners);
-
+  boolean isOwner(Set<AccountGroup.UUID> groups) {
     Set<Project.NameKey> seen = new HashSet<Project.NameKey>();
-    Project.NameKey parent = getProject().getParent();
+    seen.add(getProject().getNameKey());
 
-    while (parent != null && seen.add(parent)) {
-      ProjectState s = projectCache.get(parent);
-      if (s != null) {
-        owners.addAll(s.localOwners);
-        parent = s.getProject().getParent();
-      } else {
+    ProjectState s = this;
+    do {
+      if (CollectionsUtil.isAnyIncludedIn(s.localOwners, groups)) {
+        return true;
+      }
+
+      Project.NameKey parent = s.getProject().getParent();
+      if (parent == null || !seen.add(parent)) {
         break;
       }
-    }
-
-    return Collections.unmodifiableSet(owners);
+      s = projectCache.get(parent);
+    } while (s != null);
+    return false;
   }
 
   public ProjectControl controlFor(final CurrentUser user) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index e6b39c9..984623d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -14,22 +14,16 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.common.CollectionsUtil;
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.ParamertizedString;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
 
 import dk.brics.automaton.RegExp;
 
-import org.apache.commons.lang.StringUtils;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
@@ -39,43 +33,32 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
-import java.util.regex.Pattern;
 
 
 /** Manages access control for Git references (aka branches, tags). */
 public class RefControl {
-  public interface Factory {
-    RefControl create(ProjectControl projectControl, String ref);
-  }
-
   private final ProjectControl projectControl;
   private final String refName;
 
-  private Map<String, List<PermissionRule>> permissions;
+  /** All permissions that apply to this reference. */
+  private final PermissionCollection relevant;
+
+  /** Cached set of permissions matching this user. */
+  private final Map<String, List<PermissionRule>> effective;
 
   private Boolean owner;
   private Boolean canForgeAuthor;
   private Boolean canForgeCommitter;
 
-  @Inject
-  protected RefControl(@Assisted final ProjectControl projectControl,
-      @Assisted String ref) {
-    if (isRE(ref)) {
-      ref = shortestExample(ref);
-
-    } else if (ref.endsWith("/*")) {
-      ref = ref.substring(0, ref.length() - 1);
-
-    }
-
+  RefControl(ProjectControl projectControl, String ref,
+      PermissionCollection relevant) {
     this.projectControl = projectControl;
     this.refName = ref;
+    this.relevant = relevant;
+    this.effective = new HashMap<String, List<PermissionRule>>();
   }
 
   public String getRefName() {
@@ -87,11 +70,16 @@
   }
 
   public CurrentUser getCurrentUser() {
-    return getProjectControl().getCurrentUser();
+    return projectControl.getCurrentUser();
   }
 
-  public RefControl forUser(final CurrentUser who) {
-    return getProjectControl().forUser(who).controlForRef(getRefName());
+  public RefControl forUser(CurrentUser who) {
+    ProjectControl newCtl = projectControl.forUser(who);
+    if (relevant.isUserSpecific()) {
+      return newCtl.controlForRef(getRefName());
+    } else {
+      return new RefControl(newCtl, getRefName(), relevant);
+    }
   }
 
   /** Is this user a ref owner? */
@@ -100,16 +88,8 @@
       if (canPerform(Permission.OWNER)) {
         owner = true;
 
-      } else if (getRefName().equals(
-          AccessSection.ALL.substring(0, AccessSection.ALL.length() - 1))) {
-        // We have to prevent infinite recursion here, the project control
-        // calls us to find out if there is ownership of all references in
-        // order to determine project level ownership.
-        //
-        owner = getCurrentUser().getCapabilities().canAdministrateServer();
-
       } else {
-        owner = getProjectControl().isOwner();
+        owner = projectControl.isOwner();
       }
     }
     return owner;
@@ -117,7 +97,7 @@
 
   /** Can this user see this reference exists? */
   public boolean isVisible() {
-    return getProjectControl().visibleForReplication()
+    return projectControl.visibleForReplication()
         || canPerform(Permission.READ);
   }
 
@@ -129,14 +109,14 @@
    *         ref
    */
   public boolean canUpload() {
-    return getProjectControl()
-        .controlForRef("refs/for/" + getRefName())
-        .canPerform(Permission.PUSH);
+    return projectControl
+      .controlForRef("refs/for/" + getRefName())
+      .canPerform(Permission.PUSH);
   }
 
   /** @return true if this user can submit merge patch sets to this ref */
   public boolean canUploadMerges() {
-    return getProjectControl()
+    return projectControl
       .controlForRef("refs/for/" + getRefName())
       .canPerform(Permission.PUSH_MERGE);
   }
@@ -149,7 +129,7 @@
       // rules. Allowing this to be done by a non-project-owner opens
       // a security hole enabling editing of access rules, and thus
       // granting of powers beyond submitting to the configuration.
-      return getProjectControl().isOwner();
+      return projectControl.isOwner();
     }
     return canPerform(Permission.SUBMIT);
   }
@@ -157,7 +137,7 @@
   /** @return true if the user can update the reference as a fast-forward. */
   public boolean canUpdate() {
     if (GitRepositoryManager.REF_CONFIG.equals(refName)
-        && !getProjectControl().isOwner()) {
+        && !projectControl.isOwner()) {
       // Pushing requires being at least project owner, in addition to push.
       // Pushing configuration changes modifies the access control
       // rules. Allowing this to be done by a non-project-owner opens
@@ -175,7 +155,7 @@
 
   private boolean canPushWithForce() {
     if (GitRepositoryManager.REF_CONFIG.equals(refName)
-        && !getProjectControl().isOwner()) {
+        && !projectControl.isOwner()) {
       // Pushing requires being at least project owner, in addition to push.
       // Pushing configuration changes modifies the access control
       // rules. Allowing this to be done by a non-project-owner opens
@@ -303,9 +283,19 @@
   /** All value ranges of any allowed label permission. */
   public List<PermissionRange> getLabelRanges() {
     List<PermissionRange> r = new ArrayList<PermissionRange>();
-    for (Map.Entry<String, List<PermissionRule>> e : permissions().entrySet()) {
+    for (Map.Entry<String, List<PermissionRule>> e : relevant.getDeclaredPermissions()) {
       if (Permission.isLabel(e.getKey())) {
-        r.add(toRange(e.getKey(), e.getValue()));
+        int min = 0;
+        int max = 0;
+        for (PermissionRule rule : e.getValue()) {
+          if (projectControl.match(rule)) {
+            min = Math.min(min, rule.getMin());
+            max = Math.max(max, rule.getMax());
+          }
+        }
+        if (min != 0 || max != 0) {
+          r.add(new PermissionRange(e.getKey(), min, max));
+        }
       }
     }
     return r;
@@ -319,7 +309,8 @@
     return null;
   }
 
-  private static PermissionRange toRange(String permissionName, List<PermissionRule> ruleList) {
+  private static PermissionRange toRange(String permissionName,
+      List<PermissionRule> ruleList) {
     int min = 0;
     int max = 0;
     for (PermissionRule rule : ruleList) {
@@ -336,115 +327,41 @@
 
   /** Rules for the given permission, or the empty list. */
   private List<PermissionRule> access(String permissionName) {
-    List<PermissionRule> r = permissions().get(permissionName);
-    return r != null ? r : Collections.<PermissionRule> emptyList();
-  }
-
-  /** All rules that pertain to this user, on this reference. */
-  private Map<String, List<PermissionRule>> permissions() {
-    if (permissions == null) {
-      List<AccessSection> sections = new ArrayList<AccessSection>();
-      for (AccessSection section : projectControl.access()) {
-        if (appliesToRef(section)) {
-          sections.add(section);
-        }
-      }
-      Collections.sort(sections, new MostSpecificComparator(getRefName()));
-
-      Set<SeenRule> seen = new HashSet<SeenRule>();
-      Set<String> exclusiveGroupPermissions = new HashSet<String>();
-
-      permissions = new HashMap<String, List<PermissionRule>>();
-      for (AccessSection section : sections) {
-        for (Permission permission : section.getPermissions()) {
-          if (exclusiveGroupPermissions.contains(permission.getName())) {
-            continue;
-          }
-
-          for (PermissionRule rule : permission.getRules()) {
-            if (matchGroup(rule.getGroup().getUUID())) {
-              SeenRule s = new SeenRule(section, permission, rule);
-              if (seen.add(s) && !rule.getDeny()) {
-                List<PermissionRule> r = permissions.get(permission.getName());
-                if (r == null) {
-                  r = new ArrayList<PermissionRule>(2);
-                  permissions.put(permission.getName(), r);
-                }
-                r.add(rule);
-              }
-            }
-          }
-
-          if (permission.getExclusiveGroup()) {
-            exclusiveGroupPermissions.add(permission.getName());
-          }
-        }
-      }
-    }
-    return permissions;
-  }
-
-  private boolean appliesToRef(AccessSection section) {
-    String refPattern = section.getName();
-
-    if (isTemplate(refPattern)) {
-      ParamertizedString template = new ParamertizedString(refPattern);
-      HashMap<String, String> p = new HashMap<String, String>();
-
-      if (getCurrentUser() instanceof IdentifiedUser) {
-        p.put("username", ((IdentifiedUser) getCurrentUser()).getUserName());
-      } else {
-        // Right now we only template the username. If not available
-        // this rule cannot be matched at all.
-        //
-        return false;
-      }
-
-      if (isRE(refPattern)) {
-        for (Map.Entry<String, String> ent : p.entrySet()) {
-          ent.setValue(escape(ent.getValue()));
-        }
-      }
-
-      refPattern = template.replace(p);
+    List<PermissionRule> rules = effective.get(permissionName);
+    if (rules != null) {
+      return rules;
     }
 
-    if (isRE(refPattern)) {
-      return Pattern.matches(refPattern, getRefName());
+    rules = relevant.getPermission(permissionName);
 
-    } else if (refPattern.endsWith("/*")) {
-      String prefix = refPattern.substring(0, refPattern.length() - 1);
-      return getRefName().startsWith(prefix);
-
-    } else {
-      return getRefName().equals(refPattern);
+    if (rules.isEmpty()) {
+      effective.put(permissionName, rules);
+      return rules;
     }
-  }
 
-  private boolean matchGroup(AccountGroup.UUID uuid) {
-    Set<AccountGroup.UUID> userGroups = getCurrentUser().getEffectiveGroups();
-
-    if (AccountGroup.PROJECT_OWNERS.equals(uuid)) {
-      ProjectState state = projectControl.getProjectState();
-      return CollectionsUtil.isAnyIncludedIn(state.getAllOwners(), userGroups);
-
-    } else {
-      return userGroups.contains(uuid);
+    if (rules.size() == 1) {
+      if (!projectControl.match(rules.get(0))) {
+        rules = Collections.emptyList();
+      }
+      effective.put(permissionName, rules);
+      return rules;
     }
+
+    List<PermissionRule> mine = new ArrayList<PermissionRule>(rules.size());
+    for (PermissionRule rule : rules) {
+      if (projectControl.match(rule)) {
+        mine.add(rule);
+      }
+    }
+
+    if (mine.isEmpty()) {
+      mine = Collections.emptyList();
+    }
+    effective.put(permissionName, mine);
+    return mine;
   }
 
-  private static boolean isTemplate(String refPattern) {
-    return 0 <= refPattern.indexOf("${");
-  }
-
-  private static String escape(String value) {
-    // Right now the only special character allowed in a
-    // variable value is a . in the username.
-    //
-    return value.replace(".", "\\.");
-  }
-
-  private static boolean isRE(String refPattern) {
+  static boolean isRE(String refPattern) {
     return refPattern.startsWith(AccessSection.REGEX_PREFIX);
   }
 
@@ -458,149 +375,10 @@
     }
   }
 
-  private static RegExp toRegExp(String refPattern) {
+  static RegExp toRegExp(String refPattern) {
     if (isRE(refPattern)) {
       refPattern = refPattern.substring(1);
     }
     return new RegExp(refPattern, RegExp.NONE);
   }
-
-  /** Tracks whether or not a permission has been overridden. */
-  private static class SeenRule {
-    final String refPattern;
-    final String permissionName;
-    final AccountGroup.UUID group;
-
-    SeenRule(AccessSection section, Permission permission, PermissionRule rule) {
-      refPattern = section.getName();
-      permissionName = permission.getName();
-      group = rule.getGroup().getUUID();
-    }
-
-    @Override
-    public int hashCode() {
-      int hc = refPattern.hashCode();
-      hc = hc * 31 + permissionName.hashCode();
-      if (group != null) {
-        hc = hc * 31 + group.hashCode();
-      }
-      return hc;
-    }
-
-    @Override
-    public boolean equals(Object other) {
-      if (other instanceof SeenRule) {
-        SeenRule a = this;
-        SeenRule b = (SeenRule) other;
-        return a.refPattern.equals(b.refPattern) //
-            && a.permissionName.equals(b.permissionName) //
-            && eq(a.group, b.group);
-      }
-      return false;
-    }
-
-    private boolean eq(AccountGroup.UUID a, AccountGroup.UUID b) {
-      return a != null && b != null && a.equals(b);
-    }
-  }
-
-  /**
-   * Order the Ref Pattern by the most specific. This sort is done by:
-   * <ul>
-   * <li>1 - The minor value of Levenshtein string distance between the branch
-   * name and the regex string shortest example. A shorter distance is a more
-   * specific match.
-   * <li>2 - Finites first, infinities after.
-   * <li>3 - Number of transitions.
-   * <li>4 - Length of the expression text.
-   * </ul>
-   *
-   * Levenshtein distance is a measure of the similarity between two strings.
-   * The distance is the number of deletions, insertions, or substitutions
-   * required to transform one string into another.
-   *
-   * For example, if given refs/heads/m* and refs/heads/*, the distances are 5
-   * and 6. It means that refs/heads/m* is more specific because it's closer to
-   * refs/heads/master than refs/heads/*.
-   *
-   * Another example could be refs/heads/* and refs/heads/[a-zA-Z]*, the
-   * distances are both 6. Both are infinite, but refs/heads/[a-zA-Z]* has more
-   * transitions, which after all turns it more specific.
-   */
-  private static final class MostSpecificComparator implements
-      Comparator<AccessSection> {
-    private final String refName;
-
-    MostSpecificComparator(String refName) {
-      this.refName = refName;
-    }
-
-    public int compare(AccessSection a, AccessSection b) {
-      return compare(a.getName(), b.getName());
-    }
-
-    private int compare(final String pattern1, final String pattern2) {
-      int cmp = distance(pattern1) - distance(pattern2);
-      if (cmp == 0) {
-        boolean p1_finite = finite(pattern1);
-        boolean p2_finite = finite(pattern2);
-
-        if (p1_finite && !p2_finite) {
-          cmp = -1;
-        } else if (!p1_finite && p2_finite) {
-          cmp = 1;
-        } else /* if (f1 == f2) */{
-          cmp = 0;
-        }
-      }
-      if (cmp == 0) {
-        cmp = transitions(pattern1) - transitions(pattern2);
-      }
-      if (cmp == 0) {
-        cmp = pattern2.length() - pattern1.length();
-      }
-      return cmp;
-    }
-
-    private int distance(String pattern) {
-      String example;
-      if (isRE(pattern)) {
-        example = shortestExample(pattern);
-
-      } else if (pattern.endsWith("/*")) {
-        example = pattern.substring(0, pattern.length() - 1) + '1';
-
-      } else if (pattern.equals(refName)) {
-        return 0;
-
-      } else {
-        return Math.max(pattern.length(), refName.length());
-      }
-      return StringUtils.getLevenshteinDistance(example, refName);
-    }
-
-    private boolean finite(String pattern) {
-      if (isRE(pattern)) {
-        return toRegExp(pattern).toAutomaton().isFinite();
-
-      } else if (pattern.endsWith("/*")) {
-        return false;
-
-      } else {
-        return true;
-      }
-    }
-
-    private int transitions(String pattern) {
-      if (isRE(pattern)) {
-        return toRegExp(pattern).toAutomaton().getNumberOfTransitions();
-
-      } else if (pattern.endsWith("/*")) {
-        return pattern.length();
-
-      } else {
-        return pattern.length();
-      }
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
new file mode 100644
index 0000000..9a93725
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
@@ -0,0 +1,154 @@
+// Copyright (C) 2011 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.project;
+
+import static com.google.gerrit.server.project.RefControl.isRE;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ParamertizedString;
+
+import dk.brics.automaton.Automaton;
+
+import java.util.Collections;
+import java.util.regex.Pattern;
+
+/**
+ * Matches an AccessSection against a reference name.
+ * <p>
+ * These matchers are "compiled" versions of the AccessSection name, supporting
+ * faster selection of which sections are relevant to any given input reference.
+ */
+abstract class SectionMatcher {
+  static SectionMatcher wrap(AccessSection section) {
+    String ref = section.getName();
+    if (AccessSection.isAccessSection(ref)) {
+      return wrap(ref, section);
+    } else {
+      return null;
+    }
+  }
+
+  static SectionMatcher wrap(String pattern, AccessSection section) {
+    if (pattern.contains("${")) {
+      return new ExpandParameters(pattern, section);
+
+    } else if (isRE(pattern)) {
+      return new Regexp(pattern, section);
+
+    } else if (pattern.endsWith("/*")) {
+      return new Prefix(pattern.substring(0, pattern.length() - 1), section);
+
+    } else {
+      return new Exact(pattern, section);
+    }
+  }
+
+  final AccessSection section;
+
+  SectionMatcher(AccessSection section) {
+    this.section = section;
+  }
+
+  abstract boolean match(String ref, String username);
+
+  private static class Exact extends SectionMatcher {
+    private final String expect;
+
+    Exact(String name, AccessSection section) {
+      super(section);
+      expect = name;
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      return expect.equals(ref);
+    }
+  }
+
+  private static class Prefix extends SectionMatcher {
+    private final String prefix;
+
+    Prefix(String pfx, AccessSection section) {
+      super(section);
+      prefix = pfx;
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      return ref.startsWith(prefix);
+    }
+  }
+
+  private static class Regexp extends SectionMatcher {
+    private final Pattern pattern;
+
+    Regexp(String re, AccessSection section) {
+      super(section);
+      pattern = Pattern.compile(re);
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      return pattern.matcher(ref).matches();
+    }
+  }
+
+  static class ExpandParameters extends SectionMatcher {
+    private final ParamertizedString template;
+    private final String prefix;
+
+    ExpandParameters(String pattern, AccessSection section) {
+      super(section);
+      template = new ParamertizedString(pattern);
+
+      if (isRE(pattern)) {
+        // Replace ${username} with ":USERNAME:" as : is not legal
+        // in a reference and the string :USERNAME: is not likely to
+        // be a valid part of the regex. This later allows the pattern
+        // prefix to be clipped, saving time on evaluation.
+        Automaton am = RefControl.toRegExp(
+            template.replace(Collections.singletonMap("username", ":USERNAME:")))
+            .toAutomaton();
+        String rePrefix = am.getCommonPrefix();
+        prefix = rePrefix.substring(0, rePrefix.indexOf(":USERNAME:"));
+      } else {
+        prefix = pattern.substring(0, pattern.indexOf("${"));
+      }
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      if (!ref.startsWith(prefix) || username == null) {
+        return false;
+      }
+
+      String u;
+      if (isRE(template.getPattern())) {
+        u = username.replace(".", "\\.");
+      } else {
+        u = username;
+      }
+
+      SectionMatcher next = wrap(
+          template.replace(Collections.singletonMap("username", u)),
+          section);
+      return next != null ? next.match(ref, username) : false;
+    }
+
+   boolean matchPrefix(String ref) {
+     return ref.startsWith(prefix);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
new file mode 100644
index 0000000..c0d2034
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
@@ -0,0 +1,263 @@
+// Copyright (C) 2011 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.git;
+
+package com.google.gerrit.server.project;
+
+import static com.google.gerrit.server.project.RefControl.isRE;
+import static com.google.gerrit.server.project.RefControl.shortestExample;
+import static com.google.gerrit.server.project.RefControl.toRegExp;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.server.cache.Cache;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+
+import org.apache.commons.lang.StringUtils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+/** Caches the order AccessSections should be sorted for evaluation. */
+@Singleton
+public class SectionSortCache {
+  private static final String CACHE_NAME = "permission_sort";
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        final TypeLiteral<Cache<EntryKey, EntryVal>> type =
+            new TypeLiteral<Cache<EntryKey, EntryVal>>() {};
+        core(type, CACHE_NAME);
+        bind(SectionSortCache.class);
+      }
+    };
+  }
+
+  private final Cache<EntryKey, EntryVal> cache;
+
+  @Inject
+  SectionSortCache(@Named(CACHE_NAME) Cache<EntryKey, EntryVal> cache) {
+    this.cache = cache;
+  }
+
+  void sort(String ref, List<AccessSection> sections) {
+    final int cnt = sections.size();
+    if (cnt <= 1) {
+      return;
+    }
+
+    EntryKey key = new EntryKey(ref, sections);
+    EntryVal val = cache.get(key);
+    if (val != null) {
+      int[] srcIdx = val.order;
+      if (srcIdx != null) {
+        AccessSection[] srcList = copy(sections);
+        for (int i = 0; i < cnt; i++) {
+          sections.set(i, srcList[srcIdx[i]]);
+        }
+      } else {
+        // Identity transform. No sorting is required.
+      }
+
+    } else {
+      IdentityHashMap<AccessSection, Integer> srcMap =
+          new IdentityHashMap<AccessSection, Integer>();
+      for (int i = 0; i < cnt; i++) {
+        srcMap.put(sections.get(i), i);
+      }
+
+      Collections.sort(sections, new MostSpecificComparator(ref));
+
+      int srcIdx[];
+      if (isIdentityTransform(sections, srcMap)) {
+        srcIdx = null;
+      } else {
+        srcIdx = new int[cnt];
+        for (int i = 0; i < cnt; i++) {
+          srcIdx[i] = srcMap.get(sections.get(i));
+        }
+      }
+
+      cache.put(key, new EntryVal(srcIdx));
+    }
+  }
+
+  private static AccessSection[] copy(List<AccessSection> sections) {
+    return sections.toArray(new AccessSection[sections.size()]);
+  }
+
+  private static boolean isIdentityTransform(List<AccessSection> sections,
+      IdentityHashMap<AccessSection, Integer> srcMap) {
+    for (int i = 0; i < sections.size(); i++) {
+      if (i != srcMap.get(sections.get(i))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  static final class EntryKey {
+    private final String ref;
+    private final String[] patterns;
+    private final int hashCode;
+
+    EntryKey(String refName, List<AccessSection> sections) {
+      int hc = refName.hashCode();
+      ref = refName;
+      patterns = new String[sections.size()];
+      for (int i = 0; i < patterns.length; i++) {
+        String n = sections.get(i).getName();
+        patterns[i] = n;
+        hc = hc * 31 + n.hashCode();
+      }
+      hashCode = hc;
+    }
+
+    @Override
+    public int hashCode() {
+      return hashCode;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (other instanceof EntryKey) {
+        EntryKey b = (EntryKey) other;
+        return ref.equals(b.ref) && Arrays.equals(patterns, b.patterns);
+      }
+      return false;
+    }
+  }
+
+  static final class EntryVal {
+    /**
+     * Maps the input index to the output index.
+     * <p>
+     * For {@code x == order[y]} the expression means move the item at
+     * source position {@code x} to the output position {@code y}.
+     */
+    final int[] order;
+
+    EntryVal(int[] order) {
+      this.order = order;
+    }
+  }
+
+  /**
+   * Order the Ref Pattern by the most specific. This sort is done by:
+   * <ul>
+   * <li>1 - The minor value of Levenshtein string distance between the branch
+   * name and the regex string shortest example. A shorter distance is a more
+   * specific match.
+   * <li>2 - Finites first, infinities after.
+   * <li>3 - Number of transitions.
+   * <li>4 - Length of the expression text.
+   * </ul>
+   *
+   * Levenshtein distance is a measure of the similarity between two strings.
+   * The distance is the number of deletions, insertions, or substitutions
+   * required to transform one string into another.
+   *
+   * For example, if given refs/heads/m* and refs/heads/*, the distances are 5
+   * and 6. It means that refs/heads/m* is more specific because it's closer to
+   * refs/heads/master than refs/heads/*.
+   *
+   * Another example could be refs/heads/* and refs/heads/[a-zA-Z]*, the
+   * distances are both 6. Both are infinite, but refs/heads/[a-zA-Z]* has more
+   * transitions, which after all turns it more specific.
+   */
+  private static final class MostSpecificComparator implements
+      Comparator<AccessSection> {
+    private final String refName;
+
+    MostSpecificComparator(String refName) {
+      this.refName = refName;
+    }
+
+    public int compare(AccessSection a, AccessSection b) {
+      return compare(a.getName(), b.getName());
+    }
+
+    private int compare(final String pattern1, final String pattern2) {
+      int cmp = distance(pattern1) - distance(pattern2);
+      if (cmp == 0) {
+        boolean p1_finite = finite(pattern1);
+        boolean p2_finite = finite(pattern2);
+
+        if (p1_finite && !p2_finite) {
+          cmp = -1;
+        } else if (!p1_finite && p2_finite) {
+          cmp = 1;
+        } else /* if (f1 == f2) */{
+          cmp = 0;
+        }
+      }
+      if (cmp == 0) {
+        cmp = transitions(pattern1) - transitions(pattern2);
+      }
+      if (cmp == 0) {
+        cmp = pattern2.length() - pattern1.length();
+      }
+      return cmp;
+    }
+
+    private int distance(String pattern) {
+      String example;
+      if (isRE(pattern)) {
+        example = shortestExample(pattern);
+
+      } else if (pattern.endsWith("/*")) {
+        example = pattern.substring(0, pattern.length() - 1) + '1';
+
+      } else if (pattern.equals(refName)) {
+        return 0;
+
+      } else {
+        return Math.max(pattern.length(), refName.length());
+      }
+      return StringUtils.getLevenshteinDistance(example, refName);
+    }
+
+    private boolean finite(String pattern) {
+      if (isRE(pattern)) {
+        return toRegExp(pattern).toAutomaton().isFinite();
+
+      } else if (pattern.endsWith("/*")) {
+        return false;
+
+      } else {
+        return true;
+      }
+    }
+
+    private int transitions(String pattern) {
+      if (isRE(pattern)) {
+        return toRegExp(pattern).toAutomaton().getNumberOfTransitions();
+
+      } else if (pattern.endsWith("/*")) {
+        return pattern.length();
+
+      } else {
+        return pattern.length();
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
index ffed95a..7b8b4b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
@@ -76,11 +76,4 @@
    *        the valid status into.
    */
   public abstract void run(ApprovalType at, FunctionState state);
-
-  public boolean isValid(final CurrentUser user, final ApprovalType at,
-      final FunctionState state) {
-    return !state.controlFor(user) //
-        .getRange(Permission.forLabel(at.getCategory().getLabelName())) //
-        .isEmpty();
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
index 2cb3e81..2a871f7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
@@ -27,10 +27,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -44,7 +41,7 @@
 /** State passed through to a {@link CategoryFunction}. */
 public class FunctionState {
   public interface Factory {
-    FunctionState create(Change c, PatchSet.Id psId,
+    FunctionState create(ChangeControl c, PatchSet.Id psId,
         Collection<PatchSetApproval> all);
   }
 
@@ -55,20 +52,19 @@
       new HashMap<ApprovalCategory.Id, Collection<PatchSetApproval>>();
   private final Map<ApprovalCategory.Id, Boolean> valid =
       new HashMap<ApprovalCategory.Id, Boolean>();
+  private final ChangeControl callerChangeControl;
   private final Change change;
-  private final ProjectState project;
 
   @Inject
   FunctionState(final ApprovalTypes approvalTypes,
-      final ProjectCache projectCache,
       final IdentifiedUser.GenericFactory userFactory, final GroupCache egc,
-      @Assisted final Change c, @Assisted final PatchSet.Id psId,
+      @Assisted final ChangeControl c, @Assisted final PatchSet.Id psId,
       @Assisted final Collection<PatchSetApproval> all) {
     this.approvalTypes = approvalTypes;
     this.userFactory = userFactory;
 
-    change = c;
-    project = projectCache.get(change.getProject());
+    callerChangeControl = c;
+    change = c.getChange();
 
     for (final PatchSetApproval ca : all) {
       if (psId.equals(ca.getPatchSetId())) {
@@ -147,10 +143,8 @@
     a.setValue((short) range.squash(a.getValue()));
   }
 
-  RefControl controlFor(final CurrentUser user) {
-    ProjectControl pc = project.controlFor(user);
-    RefControl rc = pc.controlForRef(change.getDest().get());
-    return rc;
+  private ChangeControl controlFor(CurrentUser user) {
+    return callerChangeControl.forUser(user);
   }
 
   /** Run <code>applyTypeFloor</code>, <code>applyRightFloor</code>. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java
index a089817..08d0705 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.workflow;
 
 import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.server.CurrentUser;
 
 /** A function that does nothing. */
 public class NoBlock extends CategoryFunction {
@@ -25,10 +24,4 @@
   public void run(final ApprovalType at, final FunctionState state) {
     state.valid(at, true);
   }
-
-  @Override
-  public boolean isValid(final CurrentUser user, final ApprovalType at,
-      final FunctionState state) {
-    return true;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoOpFunction.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoOpFunction.java
index 6d2c26c..8c76cc5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoOpFunction.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoOpFunction.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.workflow;
 
 import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.server.CurrentUser;
 
 /** A function that does nothing. */
 public class NoOpFunction extends CategoryFunction {
@@ -24,10 +23,4 @@
   @Override
   public void run(final ApprovalType at, final FunctionState state) {
   }
-
-  @Override
-  public boolean isValid(final CurrentUser user, final ApprovalType at,
-      final FunctionState state) {
-    return false;
-  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index 90cc342..0981da1 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.cache.ConcurrentHashMapCache;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -195,6 +196,30 @@
         u.controlForRef("refs/heads/master").canUpload());
   }
 
+  public void testUsernamePatternNonRegex() {
+    grant(local, READ, devs, "refs/sb/${username}/heads/*");
+
+    ProjectControl u = user("u", devs), d = user("d", devs);
+    assertFalse("u can't read", u.controlForRef("refs/sb/d/heads/foobar").isVisible());
+    assertTrue("d can read", d.controlForRef("refs/sb/d/heads/foobar").isVisible());
+  }
+
+  public void testUsernamePatternWithRegex() {
+    grant(local, READ, devs, "^refs/sb/${username}/heads/.*");
+
+    ProjectControl u = user("d.v", devs), d = user("dev", devs);
+    assertFalse("u can't read", u.controlForRef("refs/sb/dev/heads/foobar").isVisible());
+    assertTrue("d can read", d.controlForRef("refs/sb/dev/heads/foobar").isVisible());
+  }
+
+  public void testSortWithRegex() {
+    grant(local, READ, devs, "^refs/heads/.*");
+    grant(parent, READ, anonymous, "^refs/heads/.*-QA-.*");
+
+    ProjectControl u = user(devs), d = user(devs);
+    assertTrue("u can read", u.controlForRef("refs/heads/foo-QA-bar").isVisible());
+    assertTrue("d can read", d.controlForRef("refs/heads/foo-QA-bar").isVisible());
+  }
 
   // -----------------------------------------------------------------------
 
@@ -204,6 +229,8 @@
 
   private ProjectConfig local;
   private ProjectConfig parent;
+  private PermissionCollection.Factory sectionSorter;
+
   private final AccountGroup.UUID admin = new AccountGroup.UUID("test.admin");
   private final AccountGroup.UUID anonymous = AccountGroup.ANONYMOUS_USERS;
   private final AccountGroup.UUID registered = AccountGroup.REGISTERED_USERS;
@@ -273,6 +300,11 @@
     local = new ProjectConfig(new Project.NameKey("local"));
     local.createInMemory();
     local.getProject().setParentName(parent.getProject().getName());
+
+    sectionSorter =
+        new PermissionCollection.Factory(
+            new SectionSortCache(
+                new ConcurrentHashMapCache<SectionSortCache.EntryKey, SectionSortCache.EntryVal>()));
   }
 
   private static void assertOwner(String ref, ProjectControl u) {
@@ -307,19 +339,18 @@
   }
 
   private ProjectControl user(AccountGroup.UUID... memberOf) {
+    return user(null, memberOf);
+  }
+
+  private ProjectControl user(String name, AccountGroup.UUID... memberOf) {
     SchemaFactory<ReviewDb> schema = null;
     GroupCache groupCache = null;
     String canonicalWebUrl = "http://localhost";
 
-    RefControl.Factory refControlFactory = new RefControl.Factory() {
-      @Override
-      public RefControl create(final ProjectControl projectControl, final String ref) {
-        return new RefControl(projectControl, ref);
-      }
-    };
     return new ProjectControl(Collections.<AccountGroup.UUID> emptySet(),
         Collections.<AccountGroup.UUID> emptySet(), schema, groupCache,
-        canonicalWebUrl, refControlFactory, new MockUser(memberOf),
+        sectionSorter,
+        canonicalWebUrl, new MockUser(name, memberOf),
         newProjectState());
   }
 
@@ -338,10 +369,12 @@
   }
 
   private class MockUser extends CurrentUser {
+    private final String username;
     private final Set<AccountGroup.UUID> groups;
 
-    MockUser(AccountGroup.UUID[] groupId) {
+    MockUser(String name, AccountGroup.UUID[] groupId) {
       super(RefControlTest.this.capabilityControlFactory, AccessPath.UNKNOWN);
+      username = name;
       groups = new HashSet<AccountGroup.UUID>(Arrays.asList(groupId));
       groups.add(registered);
       groups.add(anonymous);
@@ -353,6 +386,11 @@
     }
 
     @Override
+    public String getUserName() {
+      return username;
+    }
+
+    @Override
     public Set<Change.Id> getStarredChanges() {
       return Collections.emptySet();
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 0f63577..467488a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -380,7 +380,7 @@
         new PatchSetApproval(new PatchSetApproval.Key(patchSetId, currentUser
             .getAccountId(), ao.getCategoryId()), v);
     final FunctionState fs =
-        functionStateFactory.create(changeControl.getChange(), patchSetId,
+        functionStateFactory.create(changeControl, patchSetId,
             Collections.<PatchSetApproval> emptyList());
     psa.setValue(v);
     fs.normalize(approvalTypes.byId(psa.getCategoryId()), psa);