Merge branch 'stable'

* stable:
  Fix ChangeDetailFactory's invocation of PatchSetDetailFactory
  Release notes for 2.1.7.1
  Fix API breakage on ChangeDetailService
  Do not reset Patch History selection on navigation to next file diff
  Resolve Project Owners when checking access right on any ref

Conflicts:
	gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
	gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java

Change-Id: I6e956625cb4648df35035b9be2d32e6e431fb8f3
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index 71c0d1f..e00e45b 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -13,7 +13,6 @@
  [--branch <REF>] \
  [\--owner <GROUP> ...] \
  [\--parent <NAME>] \
- [\--permissions-only] \
  [\--description <DESC>] \
  [\--submit-type <TYPE>] \
  [\--use-content-merge] \
@@ -71,11 +70,6 @@
 	through. If not specified, the parent is set to the default
 	project `\-- All Projects \--`.
 
-\--permissions-only::
-	Create the project only to serve as a parent for other
-	projects.  The new project's Git repository will not be
-	initialized, and cannot be cloned.
-
 \--description::
 	Initial description of the project.  If not specified,
 	no description is stored.
diff --git a/Documentation/cmd-ls-projects.txt b/Documentation/cmd-ls-projects.txt
index 1e7b4cc..2134df8 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -8,7 +8,7 @@
 SYNOPSIS
 --------
 [verse]
-'ssh' -p <port> <host> 'gerrit ls-projects' [\--show-branch <BRANCH>]
+'ssh' -p <port> <host> 'gerrit ls-projects' [\--show-branch <BRANCH1> ...]
 
 DESCRIPTION
 -----------
@@ -30,7 +30,13 @@
 -------
 \--show-branch::
 \-b::
-	Name of the branch for which the command will display the sha of each project.
+	Branch for which the command will display the sha of each project.
+	The command may have multiple \--show-branch parameters, in this case
+	sha will be shown for each of the branches.
+	If the user does not have READ access to some branch or the branch does not
+	exist then stub (forty '\-' symbols) is shown.
+	If the user does not have access to any branch in the project then the
+	whole project is not shown.
 
 \--tree::
 \-t::
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 226ffbe..cec2783 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -485,6 +485,21 @@
 +
 Default is true, enabled.
 
+cache.projects.checkFrequency::
++
+How often project configuration should be checked for update from Git.
+Gerrit Code Review caches project access rules and configuration in
+memory, checking the refs/meta/config branch every checkFrequency
+minutes to see if a new revision should be loaded and used for future
+access. Values can be specified using standard time unit abbreviations
+('ms', 'sec', 'min', etc.).
++
+If set to 0, checks occur every time, which may slow down operations.
+Administrators may force the cache to flush with
+link:cmd-flush-caches.html[gerrit flush-caches].
++
+Default is 5 minutes.
+
 
 [[commentlink]]Section commentlink
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index 9644e92..d3094fd 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -11,7 +11,6 @@
 'java' -jar gerrit.war 'init'
 	-d <SITE_PATH>
 	[\--batch]
-	[\--import-projects]
 	[\--no-auto-start]
 
 DESCRIPTION
@@ -21,7 +20,7 @@
 into a newly created `$site_path`.
 
 If run an an existing `$site_path`, init will upgrade some resources
-as necessary.  This can be useful to import newly created projects.
+as necessary.
 
 OPTIONS
 -------
@@ -30,12 +29,6 @@
 	configuration defaults are chosen based on the whims of
 	the Gerrit developers.
 
-\--import-projects::
-	Recursively search
-	link:config-gerrit.html#gerrit.basePath[gerrit.basePath]
-	for any Git repositories not yet registered as a project,
-	and initializes a new project for them.
-
 \--no-auto-start::
 	Don't automatically start the daemon after initializing a
 	newly created site path.  This permits the administartor
diff --git a/ReleaseNotes/ReleaseNotes-2.2.0.txt b/ReleaseNotes/ReleaseNotes-2.2.0.txt
new file mode 100644
index 0000000..58605fe
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.2.0.txt
@@ -0,0 +1,65 @@
+Release notes for Gerrit 2.2.0
+==============================
+
+Gerrit 2.2.0 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.0.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.0.war]
+
+Schema Change
+-------------
+*WARNING:* Upgrading to 2.2.0 requires the server be first upgraded
+to 2.1.7, and then to 2.2.0.
+
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* The "projects" and "ref_rights" tables are no longer
+stored in the SQL database. The tables have been moved to Git
+storage, inside of the `refs/meta/config` branch of each managed
+Git repository. The init based upgrade tool will automatically
+export the current table contents and create the Git data.
+
+New Features
+------------
+
+Project Administration
+~~~~~~~~~~~~~~~~~~~~~~
+* issue 436 List projects by scanning the managed Git directory
++
+Instead of generating the list of projects from SQL database, the
+project list is obtained by recursively scanning the Git directory.
+Adding new projects is now simply a matter of creating the Git
+repository under the directory and flushing the "projects" cache
+to force the server to rescan the directory. Administrators may
+also continue to use `gerrit create-project`.
+
+* Move "projects" table into Git
++
+The projects table columns are now stored in the `project.config`
+file of the `refs/meta/config` branch of each managed Git repository.
+
+* Move "ref_rights" table into Git
++
+The "ref_rights" table is now stored in the "access" sections of
+the `project.config` file on the `refs/meta/config` branch of each
+managed Git repository. This brings version control auditing to the
+access data of each project.
+
+* New project Access screen to edit access controls
++
+The Access panel of the project administration has been rewritten
+with a new UI that reflects the new Git based storage format.
+
+Bug Fixes
+---------
+
+Project Administration
+~~~~~~~~~~~~~~~~~~~~~~
+* Avoid unnecessary updates to $GIT_DIR/description
++
+Gerrit always tried to rewrite the gitweb "description" file when the
+project was modified. This lead to unnecessary changes in the local
+filesystem, leading to more data to rsync to backups than necessary.
+Fixed to only update the file if the content changes.
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index d340a09..89d8223 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,11 @@
 Gerrit Code Review - Release Notes
 ==================================
 
+[[2_2]]
+Version 2.2.x
+-------------
+* link:ReleaseNotes-2.2.0.html[2.2.0]
+
 [[2_1]]
 Version 2.1.x
 -------------
diff --git a/gerrit-antlr/pom.xml b/gerrit-antlr/pom.xml
index 139cb30..4b9e3ad 100644
--- a/gerrit-antlr/pom.xml
+++ b/gerrit-antlr/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-antlr</artifactId>
diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml
index e878b80..814afad 100644
--- a/gerrit-common/pom.xml
+++ b/gerrit-common/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-common</artifactId>
@@ -90,6 +90,17 @@
           </execution>
         </executions>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
index efe311f..db4f348 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -54,6 +54,10 @@
     return "change," + ps.getParentKey().toString() + ",patchset=" + ps.get();
   }
 
+  public static String toProjectAcceess(final Project.NameKey p) {
+    return "admin,project," + p.get() + ",access";
+  }
+
   public static String toAccountDashboard(final AccountInfo acct) {
     return toAccountDashboard(acct.getId());
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
new file mode 100644
index 0000000..44a00d5
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
@@ -0,0 +1,140 @@
+// Copyright (C) 2010 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.common.data;
+
+import com.google.gerrit.reviewdb.Project;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/** Portion of a {@link Project} describing access rules. */
+public class AccessSection implements Comparable<AccessSection> {
+  /** Pattern that matches all references in a project. */
+  public static final String ALL = "refs/*";
+
+  /** Pattern that matches all branches in a project. */
+  public static final String HEADS = "refs/heads/*";
+
+  /** Prefix that triggers a regular expression pattern. */
+  public static final String REGEX_PREFIX = "^";
+
+  /** @return true if the name is likely to be a valid access section name. */
+  public static boolean isAccessSection(String name) {
+    return name.startsWith("refs/") || name.startsWith("^refs/");
+  }
+
+  protected String refPattern;
+  protected List<Permission> permissions;
+
+  protected AccessSection() {
+  }
+
+  public AccessSection(String refPattern) {
+    setRefPattern(refPattern);
+  }
+
+  public String getRefPattern() {
+    return refPattern;
+  }
+
+  public void setRefPattern(String refPattern) {
+    this.refPattern = refPattern;
+  }
+
+  public List<Permission> getPermissions() {
+    if (permissions == null) {
+      permissions = new ArrayList<Permission>();
+    }
+    return permissions;
+  }
+
+  public void setPermissions(List<Permission> list) {
+    Set<String> names = new HashSet<String>();
+    for (Permission p : list) {
+      if (!names.add(p.getName().toLowerCase())) {
+        throw new IllegalArgumentException();
+      }
+    }
+
+    permissions = list;
+  }
+
+  public Permission getPermission(String name) {
+    return getPermission(name, false);
+  }
+
+  public Permission getPermission(String name, boolean create) {
+    for (Permission p : getPermissions()) {
+      if (p.getName().equalsIgnoreCase(name)) {
+        return p;
+      }
+    }
+
+    if (create) {
+      Permission p = new Permission(name);
+      permissions.add(p);
+      return p;
+    } else {
+      return null;
+    }
+  }
+
+  public void remove(Permission permission) {
+    if (permission != null) {
+      removePermission(permission.getName());
+    }
+  }
+
+  public void removePermission(String name) {
+    if (permissions != null) {
+      for (Iterator<Permission> itr = permissions.iterator(); itr.hasNext();) {
+        if (name.equalsIgnoreCase(itr.next().getName())) {
+          itr.remove();
+        }
+      }
+    }
+  }
+
+  public void mergeFrom(AccessSection section) {
+    for (Permission src : section.getPermissions()) {
+      Permission dst = getPermission(src.getName());
+      if (dst != null) {
+        dst.mergeFrom(src);
+      } else {
+        permissions.add(dst);
+      }
+    }
+  }
+
+  @Override
+  public int compareTo(AccessSection o) {
+    return comparePattern().compareTo(o.comparePattern());
+  }
+
+  private String comparePattern() {
+    if (getRefPattern().startsWith(REGEX_PREFIX)) {
+      return getRefPattern().substring(REGEX_PREFIX.length());
+    }
+    return getRefPattern();
+  }
+
+  @Override
+  public String toString() {
+    return "AccessSection[" + getRefPattern() + "]";
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalType.java
index ea9aedb..7d03457 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalType.java
@@ -31,6 +31,7 @@
   protected short maxNegative;
   protected short maxPositive;
 
+  private transient List<Integer> intList;
   private transient Map<Short, ApprovalCategoryValue> byValue;
 
   protected ApprovalType() {
@@ -56,6 +57,9 @@
         maxPositive = values.get(values.size() - 1).getValue();
       }
     }
+
+    // Force the label name to pre-compute so we don't have data race conditions.
+    getCategory().getLabelName();
   }
 
   public ApprovalCategory getCategory() {
@@ -107,4 +111,16 @@
       }
     }
   }
+
+  public List<Integer> getValuesAsList() {
+    if (intList == null) {
+      intList = new ArrayList<Integer>(values.size());
+      for (ApprovalCategoryValue acv : values) {
+        intList.add(Integer.valueOf(acv.getValue()));
+      }
+      Collections.sort(intList);
+      Collections.reverse(intList);
+    }
+    return intList;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalTypes.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalTypes.java
index 1b6d4a3..0518010 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalTypes.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalTypes.java
@@ -19,20 +19,17 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 public class ApprovalTypes {
   protected List<ApprovalType> approvalTypes;
-  protected List<ApprovalType> actionTypes;
-  private transient Map<ApprovalCategory.Id, ApprovalType> byCategoryId;
+  private transient Map<ApprovalCategory.Id, ApprovalType> byId;
+  private transient Map<String, ApprovalType> byLabel;
 
   protected ApprovalTypes() {
   }
 
-  public ApprovalTypes(final List<ApprovalType> approvals,
-      final List<ApprovalType> actions) {
+  public ApprovalTypes(final List<ApprovalType> approvals) {
     approvalTypes = approvals;
-    actionTypes = actions;
     byCategory();
   }
 
@@ -40,33 +37,35 @@
     return approvalTypes;
   }
 
-  public List<ApprovalType> getActionTypes() {
-    return actionTypes;
-  }
-
-  public ApprovalType getApprovalType(final ApprovalCategory.Id id) {
+  public ApprovalType byId(final ApprovalCategory.Id id) {
     return byCategory().get(id);
   }
 
-  public Set<ApprovalCategory.Id> getApprovalCategories() {
-    return byCategory().keySet();
-  }
-
   private Map<ApprovalCategory.Id, ApprovalType> byCategory() {
-    if (byCategoryId == null) {
-      byCategoryId = new HashMap<ApprovalCategory.Id, ApprovalType>();
-      if (actionTypes != null) {
-        for (final ApprovalType t : actionTypes) {
-          byCategoryId.put(t.getCategory().getId(), t);
-        }
-      }
-
+    if (byId == null) {
+      byId = new HashMap<ApprovalCategory.Id, ApprovalType>();
       if (approvalTypes != null) {
         for (final ApprovalType t : approvalTypes) {
-          byCategoryId.put(t.getCategory().getId(), t);
+          byId.put(t.getCategory().getId(), t);
         }
       }
     }
-    return byCategoryId;
+    return byId;
+  }
+
+  public ApprovalType byLabel(String labelName) {
+    return byLabel().get(labelName.toLowerCase());
+  }
+
+  private Map<String, ApprovalType> byLabel() {
+    if (byLabel == null) {
+      byLabel = new HashMap<String, ApprovalType>();
+      if (approvalTypes != null) {
+        for (ApprovalType t : approvalTypes) {
+          byLabel.put(t.getCategory().getLabelName().toLowerCase(), t);
+        }
+      }
+    }
+    return byLabel;
   }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
index 572fd7a..f196c05 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
@@ -39,10 +39,10 @@
   protected List<PatchSet> patchSets;
   protected List<ApprovalDetail> approvals;
   protected Set<ApprovalCategory.Id> missingApprovals;
+  protected boolean canSubmit;
   protected List<ChangeMessage> messages;
   protected PatchSet.Id currentPatchSetId;
   protected PatchSetDetail currentDetail;
-  protected Set<ApprovalCategory.Id> currentActions;
 
   public ChangeDetail() {
   }
@@ -87,6 +87,14 @@
       canRevert = a;
   }
 
+  public boolean canSubmit() {
+    return canSubmit;
+  }
+
+  public void setCanSubmit(boolean a) {
+    canSubmit = a;
+  }
+
   public Change getChange() {
     return change;
   }
@@ -153,14 +161,6 @@
     missingApprovals = a;
   }
 
-  public Set<ApprovalCategory.Id> getCurrentActions() {
-    return currentActions;
-  }
-
-  public void setCurrentActions(Set<ApprovalCategory.Id> a) {
-    currentActions = a;
-  }
-
   public boolean isCurrentPatchSet(final PatchSetDetail detail) {
     return currentPatchSetId != null
         && detail.getPatchSet().getId().equals(currentPatchSetId);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
index ce508cc..6d4b889 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
@@ -21,8 +21,8 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtjsonrpc.client.RemoteJsonService;
 import com.google.gwtjsonrpc.client.RpcImpl;
-import com.google.gwtjsonrpc.client.VoidResult;
 import com.google.gwtjsonrpc.client.RpcImpl.Version;
+import com.google.gwtjsonrpc.client.VoidResult;
 
 import java.util.List;
 import java.util.Set;
@@ -36,7 +36,8 @@
   void createGroup(String newName, AsyncCallback<AccountGroup.Id> callback);
 
   @SignInRequired
-  void groupDetail(AccountGroup.Id groupId, AsyncCallback<GroupDetail> callback);
+  void groupDetail(AccountGroup.Id groupId, AccountGroup.UUID uuid,
+      AsyncCallback<GroupDetail> callback);
 
   @SignInRequired
   void changeGroupDescription(AccountGroup.Id groupId, String description,
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
new file mode 100644
index 0000000..cd6e172
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2010 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.common.data;
+
+import com.google.gerrit.reviewdb.AccountGroup;
+
+/** Describes a group within a projects {@link AccessSection}s. */
+public class GroupReference implements Comparable<GroupReference> {
+  /** @return a new reference to the given group description. */
+  public static GroupReference forGroup(AccountGroup group) {
+    return new GroupReference(group.getGroupUUID(), group.getName());
+  }
+
+  protected String uuid;
+  protected String name;
+
+  protected GroupReference() {
+  }
+
+  public GroupReference(AccountGroup.UUID uuid, String name) {
+    setUUID(uuid);
+    setName(name);
+  }
+
+  public AccountGroup.UUID getUUID() {
+    return uuid != null ? new AccountGroup.UUID(uuid) : null;
+  }
+
+  public void setUUID(AccountGroup.UUID newUUID) {
+    uuid = newUUID != null ? newUUID.get() : null;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String newName) {
+    this.name = newName;
+  }
+
+  @Override
+  public int compareTo(GroupReference o) {
+    return uuid(this).compareTo(uuid(o));
+  }
+
+  private static String uuid(GroupReference a) {
+    return a.getUUID() != null ? a.getUUID().get() : "?";
+  }
+
+  @Override
+  public int hashCode() {
+    return uuid(this).hashCode();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return o instanceof GroupReference && compareTo((GroupReference) o) == 0;
+  }
+
+  @Override
+  public String toString() {
+    return "Group[" + getName() + " / " + getUUID() + "]";
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/InheritedRefRight.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/InheritedRefRight.java
deleted file mode 100644
index 4dc998b..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/InheritedRefRight.java
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (C) 2010 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.common.data;
-
-import com.google.gerrit.reviewdb.RefRight;
-
-/**
- * Additional data about a {@link RefRight} not normally loaded: defines if a
- * right is inherited from a parent structure (e.g. a parent project).
- */
-public class InheritedRefRight {
-  private RefRight right;
-  private boolean inherited;
-  private boolean owner;
-
-  /**
-   * Creates a instance of a {@link RefRight} with data about inheritance
-   */
-  protected InheritedRefRight() {
-  }
-
-  /**
-   * Creates a instance of a {@link RefRight} with data about inheritance
-   *
-   * @param right the right
-   * @param inherited true if the right is inherited, false otherwise
-   * @param owner true if right is owned by current user, false otherwise
-   */
-  public InheritedRefRight(RefRight right, boolean inherited, boolean owner) {
-    this.right = right;
-    this.inherited = inherited;
-    this.owner = owner;
-  }
-
-  public RefRight getRight() {
-    return right;
-  }
-
-  public boolean isInherited() {
-    return inherited;
-  }
-
-  public boolean isOwner() {
-    return owner;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o instanceof InheritedRefRight) {
-      InheritedRefRight a = this;
-      InheritedRefRight b = (InheritedRefRight) o;
-      return a.getRight().equals(b.getRight())
-          && a.isInherited() == b.isInherited();
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    return getRight().hashCode();
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index 24cdee4..2214af9 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
@@ -155,6 +155,10 @@
     return intralineFailure;
   }
 
+  public boolean isExpandAllComments() {
+    return diffPrefs.isExpandAllComments();
+  }
+
   public SparseFileContent getA() {
     return a;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
index 7a0ada3..273f18d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
@@ -15,39 +15,35 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchLineComment;
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gerrit.reviewdb.PatchSetInfo;
 
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 public class PatchSetPublishDetail {
   protected AccountInfoCache accounts;
   protected PatchSetInfo patchSetInfo;
   protected Change change;
   protected List<PatchLineComment> drafts;
-  protected Map<ApprovalCategory.Id, Set<ApprovalCategoryValue.Id>> allowed;
-  protected Map<ApprovalCategory.Id, PatchSetApproval> given;
-  protected boolean isSubmitAllowed;
+  protected List<PermissionRange> labels;
+  protected List<PatchSetApproval> given;
+  protected boolean canSubmit;
 
-  public Map<ApprovalCategory.Id, Set<ApprovalCategoryValue.Id>> getAllowed() {
-    return allowed;
+  public List<PermissionRange> getLabels() {
+    return labels;
   }
 
-  public void setAllowed(
-      Map<ApprovalCategory.Id, Set<ApprovalCategoryValue.Id>> allowed) {
-    this.allowed = allowed;
+  public void setLabels(List<PermissionRange> labels) {
+    this.labels = labels;
   }
 
-  public Map<ApprovalCategory.Id, PatchSetApproval> getGiven() {
+  public List<PatchSetApproval> getGiven() {
     return given;
   }
 
-  public void setGiven(Map<ApprovalCategory.Id, PatchSetApproval> given) {
+  public void setGiven(List<PatchSetApproval> given) {
     this.given = given;
   }
 
@@ -67,8 +63,8 @@
     this.drafts = drafts;
   }
 
-  public void setSubmitAllowed(boolean allowed) {
-    isSubmitAllowed = allowed;
+  public void setCanSubmit(boolean allowed) {
+    canSubmit = allowed;
   }
 
   public AccountInfoCache getAccounts() {
@@ -87,20 +83,25 @@
     return drafts;
   }
 
-  public boolean isAllowed(final ApprovalCategory.Id id) {
-    final Set<ApprovalCategoryValue.Id> s = getAllowed(id);
-    return s != null && !s.isEmpty();
+  public PermissionRange getRange(final String permissionName) {
+    for (PermissionRange s : labels) {
+      if (s.getName().equals(permissionName)) {
+        return s;
+      }
+    }
+    return null;
   }
 
-  public Set<ApprovalCategoryValue.Id> getAllowed(final ApprovalCategory.Id id) {
-    return allowed.get(id);
+  public PatchSetApproval getChangeApproval(ApprovalCategory.Id id) {
+    for (PatchSetApproval a : given) {
+      if (a.getCategoryId().equals(id)) {
+        return a;
+      }
+    }
+    return null;
   }
 
-  public PatchSetApproval getChangeApproval(final ApprovalCategory.Id id) {
-    return given.get(id);
-  }
-
-  public boolean isSubmitAllowed() {
-    return isSubmitAllowed;
+  public boolean canSubmit() {
+    return canSubmit;
   }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
new file mode 100644
index 0000000..c27d9d9
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -0,0 +1,201 @@
+// Copyright (C) 2010 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.common.data;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/** A single permission within an {@link AccessSection} of a project. */
+public class Permission implements Comparable<Permission> {
+  public static final String CREATE = "create";
+  public static final String FORGE_AUTHOR = "forgeAuthor";
+  public static final String FORGE_COMMITTER = "forgeCommitter";
+  public static final String FORGE_SERVER = "forgeServerAsCommitter";
+  public static final String LABEL = "label-";
+  public static final String OWNER = "owner";
+  public static final String PUSH = "push";
+  public static final String PUSH_MERGE = "pushMerge";
+  public static final String PUSH_TAG = "pushTag";
+  public static final String READ = "read";
+  public static final String SUBMIT = "submit";
+
+  private static final List<String> NAMES_LC;
+
+  static {
+    NAMES_LC = new ArrayList<String>();
+    NAMES_LC.add(OWNER.toLowerCase());
+    NAMES_LC.add(READ.toLowerCase());
+    NAMES_LC.add(CREATE.toLowerCase());
+    NAMES_LC.add(FORGE_AUTHOR.toLowerCase());
+    NAMES_LC.add(FORGE_COMMITTER.toLowerCase());
+    NAMES_LC.add(FORGE_SERVER.toLowerCase());
+    NAMES_LC.add(PUSH.toLowerCase());
+    NAMES_LC.add(PUSH_MERGE.toLowerCase());
+    NAMES_LC.add(PUSH_TAG.toLowerCase());
+    NAMES_LC.add(LABEL.toLowerCase());
+    NAMES_LC.add(SUBMIT.toLowerCase());
+  }
+
+  /** @return true if the name is recognized as a permission name. */
+  public static boolean isPermission(String varName) {
+    String lc = varName.toLowerCase();
+    if (lc.startsWith(LABEL)) {
+      return LABEL.length() < lc.length();
+    }
+    return NAMES_LC.contains(lc);
+  }
+
+  /** @return true if the permission name is actually for a review label. */
+  public static boolean isLabel(String varName) {
+    return varName.startsWith(LABEL) && LABEL.length() < varName.length();
+  }
+
+  /** @return permission name for the given review label. */
+  public static String forLabel(String labelName) {
+    return LABEL + labelName;
+  }
+
+  protected String name;
+  protected boolean exclusiveGroup;
+  protected List<PermissionRule> rules;
+
+  protected Permission() {
+  }
+
+  public Permission(String name) {
+    this.name = name;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public boolean isLabel() {
+    return isLabel(getName());
+  }
+
+  public String getLabel() {
+    if (isLabel()) {
+      return getName().substring(LABEL.length());
+    }
+    return null;
+  }
+
+  public Boolean getExclusiveGroup() {
+    // Only permit exclusive group behavior on non OWNER permissions,
+    // otherwise an owner might lose access to a delegated subspace.
+    //
+    return exclusiveGroup && !OWNER.equals(getName());
+  }
+
+  public void setExclusiveGroup(Boolean newExclusiveGroup) {
+    exclusiveGroup = newExclusiveGroup;
+  }
+
+  public List<PermissionRule> getRules() {
+    initRules();
+    return rules;
+  }
+
+  public void setRules(List<PermissionRule> list) {
+    rules = list;
+  }
+
+  public void add(PermissionRule rule) {
+    initRules();
+    rules.add(rule);
+  }
+
+  public void remove(PermissionRule rule) {
+    if (rule != null) {
+      removeRule(rule.getGroup());
+    }
+  }
+
+  public void removeRule(GroupReference group) {
+    if (rules != null) {
+      for (Iterator<PermissionRule> itr = rules.iterator(); itr.hasNext();) {
+        if (sameGroup(itr.next(), group)) {
+          itr.remove();
+        }
+      }
+    }
+  }
+
+  public PermissionRule getRule(GroupReference group) {
+    return getRule(group, false);
+  }
+
+  public PermissionRule getRule(GroupReference group, boolean create) {
+    initRules();
+
+    for (PermissionRule r : rules) {
+      if (sameGroup(r, group)) {
+        return r;
+      }
+    }
+
+    if (create) {
+      PermissionRule r = new PermissionRule(group);
+      rules.add(r);
+      return r;
+    } else {
+      return null;
+    }
+  }
+
+  void mergeFrom(Permission src) {
+    for (PermissionRule srcRule : src.getRules()) {
+      PermissionRule dstRule = getRule(srcRule.getGroup());
+      if (dstRule != null) {
+        dstRule.mergeFrom(srcRule);
+      } else {
+        add(srcRule);
+      }
+    }
+  }
+
+  private static boolean sameGroup(PermissionRule rule, GroupReference group) {
+    if (group.getUUID() != null) {
+      return group.getUUID().equals(rule.getGroup().getUUID());
+
+    } else if (group.getName() != null) {
+      return group.getName().equals(rule.getGroup().getName());
+
+    } else {
+      return false;
+    }
+  }
+
+  private void initRules() {
+    if (rules == null) {
+      rules = new ArrayList<PermissionRule>(4);
+    }
+  }
+
+  @Override
+  public int compareTo(Permission b) {
+    int cmp = index(this) - index(b);
+    if (cmp == 0) getName().compareTo(b.getName());
+    return cmp;
+  }
+
+  private static int index(Permission a) {
+    String lc = a.isLabel() ? Permission.LABEL : a.getName().toLowerCase();
+    int index = NAMES_LC.indexOf(lc);
+    return 0 <= index ? index : NAMES_LC.size();
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java
new file mode 100644
index 0000000..bc2dadd
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java
@@ -0,0 +1,95 @@
+// Copyright (C) 2010 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.common.data;
+
+public class PermissionRange implements Comparable<PermissionRange> {
+  protected String name;
+  protected int min;
+  protected int max;
+
+  protected PermissionRange() {
+  }
+
+  public PermissionRange(String name, int min, int max) {
+    this.name = name;
+
+    if (min <= max) {
+      this.min = min;
+      this.max = max;
+    } else {
+      this.min = max;
+      this.max = min;
+    }
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public boolean isLabel() {
+    return Permission.isLabel(getName());
+  }
+
+  public String getLabel() {
+    return isLabel() ? getName().substring(Permission.LABEL.length()) : null;
+  }
+
+  public int getMin() {
+    return min;
+  }
+
+  public int getMax() {
+    return max;
+  }
+
+  /** True if the value is within the range. */
+  public boolean contains(int value) {
+    return getMin() <= value && value <= getMax();
+  }
+
+  /** Normalize the value to fit within the bounds of the range. */
+  public int squash(int value) {
+    return Math.min(Math.max(getMin(), value), getMax());
+  }
+
+  /** True both {@link #getMin()} and {@link #getMax()} are 0. */
+  public boolean isEmpty() {
+    return getMin() == 0 && getMax() == 0;
+  }
+
+  @Override
+  public int compareTo(PermissionRange o) {
+    return getName().compareTo(o.getName());
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder r = new StringBuilder();
+    if (getMin() < 0 && getMax() == 0) {
+      r.append(getMin());
+      r.append(' ');
+    } else {
+      if (getMin() != getMax()) {
+        if (0 <= getMin()) r.append('+');
+        r.append(getMin());
+        r.append("..");
+      }
+      if (0 <= getMax()) r.append('+');
+      r.append(getMax());
+      r.append(' ');
+    }
+    return r.toString();
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
new file mode 100644
index 0000000..5c73c52
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
@@ -0,0 +1,203 @@
+// Copyright (C) 2010 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.common.data;
+
+public class PermissionRule implements Comparable<PermissionRule> {
+  public static enum Action {
+    ALLOW, DENY;
+  }
+
+  protected boolean deny;
+  protected boolean force;
+  protected int min;
+  protected int max;
+  protected GroupReference group;
+
+  public PermissionRule() {
+  }
+
+  public PermissionRule(GroupReference group) {
+    this.group = group;
+  }
+
+  public Action getAction() {
+    return deny ? Action.DENY : Action.ALLOW;
+  }
+
+  public void setAction(Action action) {
+    if (action == null) {
+      throw new NullPointerException("action");
+    }
+    setDeny(action == Action.DENY);
+  }
+
+  public boolean getDeny() {
+    return deny;
+  }
+
+  public void setDeny(boolean newDeny) {
+    deny = newDeny;
+  }
+
+  public Boolean getForce() {
+    return force;
+  }
+
+  public void setForce(Boolean newForce) {
+    force = newForce;
+  }
+
+  public Integer getMin() {
+    return min;
+  }
+
+  public void setMin(Integer min) {
+    this.min = min;
+  }
+
+  public void setMax(Integer max) {
+    this.max = max;
+  }
+
+  public Integer getMax() {
+    return max;
+  }
+
+  public void setRange(int newMin, int newMax) {
+    if (newMax < newMin) {
+      min = newMax;
+      max = newMin;
+    } else {
+      min = newMin;
+      max = newMax;
+    }
+  }
+
+  public GroupReference getGroup() {
+    return group;
+  }
+
+  public void setGroup(GroupReference newGroup) {
+    group = newGroup;
+  }
+
+  void mergeFrom(PermissionRule src) {
+    setDeny(getDeny() || src.getDeny());
+    setForce(getForce() || src.getForce());
+    setRange(Math.min(getMin(), src.getMin()), Math.max(getMax(), src.getMax()));
+  }
+
+  @Override
+  public int compareTo(PermissionRule o) {
+    int cmp = deny(this) - deny(o);
+    if (cmp == 0) cmp = range(o) - range(this);
+    if (cmp == 0) cmp = group(this).compareTo(group(o));
+    return cmp;
+  }
+
+  private static int deny(PermissionRule a) {
+    return a.getDeny() ? 1 : 0;
+  }
+
+  private static int range(PermissionRule a) {
+    return Math.abs(a.getMin()) + Math.abs(a.getMax());
+  }
+
+  private static String group(PermissionRule a) {
+    return a.getGroup().getName() != null ? a.getGroup().getName() : "";
+  }
+
+  @Override
+  public String toString() {
+    return asString(true);
+  }
+
+  public String asString(boolean canUseRange) {
+    StringBuilder r = new StringBuilder();
+
+    if (getDeny()) {
+      r.append("deny ");
+    }
+
+    if (getForce()) {
+      r.append("+force ");
+    }
+
+    if (canUseRange && (getMin() != 0 || getMax() != 0)) {
+      if (0 <= getMin()) r.append('+');
+      r.append(getMin());
+      r.append("..");
+      if (0 <= getMax()) r.append('+');
+      r.append(getMax());
+      r.append(' ');
+    }
+
+    r.append("group ");
+    r.append(getGroup().getName());
+
+    return r.toString();
+  }
+
+  public static PermissionRule fromString(String src, boolean mightUseRange) {
+    final String orig = src;
+    final PermissionRule rule = new PermissionRule();
+
+    src = src.trim();
+
+    if (src.startsWith("deny ")) {
+      rule.setDeny(true);
+      src = src.substring(5).trim();
+    }
+
+    if (src.startsWith("+force ")) {
+      rule.setForce(true);
+      src = src.substring("+force ".length()).trim();
+    }
+
+    if (mightUseRange && !src.startsWith("group ")) {
+      int sp = src.indexOf(' ');
+      String range = src.substring(0, sp);
+
+      if (range.matches("^([+-]\\d+)\\.\\.([+-]\\d)$")) {
+        int dotdot = range.indexOf("..");
+        int min = parseInt(range.substring(0, dotdot));
+        int max = parseInt(range.substring(dotdot + 2));
+        rule.setRange(min, max);
+      } else {
+        throw new IllegalArgumentException("Invalid range in rule: " + orig);
+      }
+
+      src = src.substring(sp + 1).trim();
+    }
+
+    if (src.startsWith("group ")) {
+      src = src.substring(6).trim();
+      GroupReference group = new GroupReference();
+      group.setName(src);
+      rule.setGroup(group);
+    } else {
+      throw new IllegalArgumentException("Rule must include group: " + orig);
+    }
+
+    return rule;
+  }
+
+  private static int parseInt(String value) {
+    if (value.startsWith("+")) {
+      value = value.substring(1);
+    }
+    return Integer.parseInt(value);
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
new file mode 100644
index 0000000..2fbf512
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
@@ -0,0 +1,66 @@
+// 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.common.data;
+
+import com.google.gerrit.reviewdb.Project;
+
+import java.util.List;
+import java.util.Set;
+
+public class ProjectAccess {
+  protected String revision;
+  protected Project.NameKey inheritsFrom;
+  protected List<AccessSection> local;
+  protected Set<String> ownerOf;
+
+  public ProjectAccess() {
+  }
+
+  public String getRevision() {
+    return revision;
+  }
+
+  public void setRevision(String name) {
+    revision = name;
+  }
+
+  public Project.NameKey getInheritsFrom() {
+    return inheritsFrom;
+  }
+
+  public void setInheritsFrom(Project.NameKey name) {
+    inheritsFrom = name;
+  }
+
+  public List<AccessSection> getLocal() {
+    return local;
+  }
+
+  public void setLocal(List<AccessSection> as) {
+    local = as;
+  }
+
+  public boolean isOwnerOf(AccessSection section) {
+    return getOwnerOf().contains(section.getRefPattern());
+  }
+
+  public Set<String> getOwnerOf() {
+    return ownerOf;
+  }
+
+  public void setOwnerOf(Set<String> refs) {
+    ownerOf = refs;
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
index b5a986f..cd02785 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAdminService.java
@@ -15,10 +15,8 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtjsonrpc.client.RemoteJsonService;
 import com.google.gwtjsonrpc.client.RpcImpl;
@@ -34,18 +32,17 @@
   void projectDetail(Project.NameKey projectName,
       AsyncCallback<ProjectDetail> callback);
 
+  void projectAccess(Project.NameKey projectName,
+      AsyncCallback<ProjectAccess> callback);
+
   @SignInRequired
   void changeProjectSettings(Project update,
       AsyncCallback<ProjectDetail> callback);
 
   @SignInRequired
-  void deleteRight(Project.NameKey projectName, Set<RefRight.Key> ids,
-      AsyncCallback<ProjectDetail> callback);
-
-  @SignInRequired
-  void addRight(Project.NameKey projectName, ApprovalCategory.Id categoryId,
-      String groupName, String refName, short min, short max,
-      AsyncCallback<ProjectDetail> callback);
+  void changeProjectAccess(Project.NameKey projectName, String baseRevision,
+      String message, List<AccessSection> sections,
+      AsyncCallback<ProjectAccess> callback);
 
   void listBranches(Project.NameKey projectName,
       AsyncCallback<ListBranchesResult> callback);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectDetail.java
index 2aa8c62..02aaf80 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectDetail.java
@@ -14,16 +14,10 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.Project;
 
-import java.util.List;
-import java.util.Map;
-
 public class ProjectDetail {
   public Project project;
-  public Map<AccountGroup.Id, AccountGroup> groups;
-  public List<InheritedRefRight> rights;
   public boolean canModifyDescription;
   public boolean canModifyMergeType;
   public boolean canModifyAgreements;
@@ -36,14 +30,6 @@
     project = p;
   }
 
-  public void setGroups(final Map<AccountGroup.Id, AccountGroup> g) {
-    groups = g;
-  }
-
-  public void setRights(final List<InheritedRefRight> r) {
-    rights = r;
-  }
-
   public void setCanModifyDescription(final boolean cmd) {
     canModifyDescription = cmd;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
index 9dae169..976004f 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.AccountGroupName;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtjsonrpc.client.RemoteJsonService;
@@ -32,5 +31,5 @@
       AsyncCallback<List<AccountInfo>> callback);
 
   void suggestAccountGroup(String query, int limit,
-      AsyncCallback<List<AccountGroupName>> callback);
+      AsyncCallback<List<GroupReference>> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java
index 4e117b1..179bc3a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java
@@ -26,10 +26,18 @@
     this(key, null);
   }
 
+  public NoSuchGroupException(final AccountGroup.UUID key) {
+    this(key, null);
+  }
+
   public NoSuchGroupException(final AccountGroup.Id key, final Throwable why) {
     super(MESSAGE + key.toString(), why);
   }
 
+  public NoSuchGroupException(final AccountGroup.UUID key, final Throwable why) {
+    super(MESSAGE + key.toString(), why);
+  }
+
   public NoSuchGroupException(final AccountGroup.NameKey k) {
     this(k, null);
   }
diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml
index 88246ac..480c112 100644
--- a/gerrit-gwtdebug/pom.xml
+++ b/gerrit-gwtdebug/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtdebug</artifactId>
diff --git a/gerrit-gwtui/pom.xml b/gerrit-gwtui/pom.xml
index f1e7438..70c5740 100644
--- a/gerrit-gwtui/pom.xml
+++ b/gerrit-gwtui/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtui</artifactId>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
index ec0f74f..d3d990c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
@@ -14,6 +14,7 @@
  limitations under the License.
 -->
 <module rename-to="gerrit">
+  <inherits name='com.google.gwt.editor.Editor'/>
   <inherits name='com.google.gwt.user.User'/>
   <inherits name='com.google.gwt.resources.Resources'/>
   <inherits name='com.google.gwt.user.theme.chrome.Chrome'/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index 2b3ace4..8b82a30 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -87,6 +87,10 @@
     return "admin,group," + id.toString();
   }
 
+  public static String toGroup(final AccountGroup.UUID uuid) {
+    return "admin,group,uuid-" + uuid.toString();
+  }
+
   public static String toProjectAdmin(final Project.NameKey n, final String tab) {
     return "admin,project," + n.toString() + "," + tab;
   }
@@ -411,6 +415,10 @@
       private Screen select() {
         String p;
 
+        p = "admin,group,uuid-";
+        if (token.startsWith(p))
+          return new AccountGroupScreen(AccountGroup.UUID.parse(skip(p, token)));
+
         p = "admin,group,";
         if (token.startsWith(p))
           return new AccountGroupScreen(AccountGroup.Id.parse(skip(p, token)));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessRightEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessRightEditor.java
deleted file mode 100644
index 8f6831b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessRightEditor.java
+++ /dev/null
@@ -1,404 +0,0 @@
-// Copyright (C) 2010 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.client.admin;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
-import com.google.gerrit.client.ui.HintTextBox;
-import com.google.gerrit.client.ui.RPCSuggestOracle;
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gwt.event.dom.client.ChangeEvent;
-import com.google.gwt.event.dom.client.ChangeHandler;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.SuggestBox;
-
-public class AccessRightEditor extends Composite
-    implements HasValueChangeHandlers<ProjectDetail> {
-  private Project.NameKey projectKey;
-  private ListBox catBox;
-  private HintTextBox nameTxt;
-  private SuggestBox nameSug;
-  private HintTextBox referenceTxt;
-  private ListBox topBox;
-  private ListBox botBox;
-  private Button addBut;
-  private Button clearBut;
-
-  public AccessRightEditor(final Project.NameKey key) {
-    projectKey = key;
-
-    initWidgets();
-    initCategories();
-
-    final Grid grid = new Grid(5, 2);
-    grid.setText(0, 0, Util.C.columnApprovalCategory() + ":");
-    grid.setWidget(0, 1, catBox);
-
-    grid.setText(1, 0, Util.C.columnGroupName() + ":");
-    grid.setWidget(1, 1, nameSug);
-
-    grid.setText(2, 0, Util.C.columnRefName() + ":");
-    grid.setWidget(2, 1, referenceTxt);
-
-    grid.setText(3, 0, Util.C.columnRightRange() + ":");
-    grid.setWidget(3, 1, topBox);
-
-    grid.setText(4, 0, "");
-    grid.setWidget(4, 1, botBox);
-
-    FlowPanel fp = new FlowPanel();
-    fp.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
-
-    fp.add(grid);
-    fp.add(addBut);
-    fp.add(clearBut);
-    initWidget(fp);
-  }
-
-  protected void initWidgets() {
-    catBox = new ListBox();
-    catBox.addChangeHandler(new ChangeHandler() {
-      @Override
-      public void onChange(final ChangeEvent event) {
-        updateCategorySelection();
-      }
-    });
-
-    nameTxt = new HintTextBox();
-    nameSug = new SuggestBox(new RPCSuggestOracle(
-        new AccountGroupSuggestOracle()), nameTxt);
-    nameTxt.setVisibleLength(50);
-    nameTxt.setHintText(Util.C.defaultAccountGroupName());
-
-    referenceTxt = new HintTextBox();
-    referenceTxt.setVisibleLength(50);
-    referenceTxt.setText("");
-    referenceTxt.addKeyPressHandler(new KeyPressHandler() {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-          doAddNewRight();
-        }
-      }
-    });
-
-    topBox = new ListBox();
-    botBox = new ListBox();
-
-    addBut = new Button(Util.C.buttonAddProjectRight());
-    addBut.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        doAddNewRight();
-      }
-    });
-
-    clearBut = new Button(Util.C.buttonClearProjectRight());
-    clearBut.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        clear();
-      }
-    });
-  }
-
-  protected void initCategories() {
-    for (final ApprovalType at : Gerrit.getConfig().getApprovalTypes()
-        .getApprovalTypes()) {
-      final ApprovalCategory c = at.getCategory();
-      catBox.addItem(c.getName(), c.getId().get());
-    }
-    for (final ApprovalType at : Gerrit.getConfig().getApprovalTypes()
-        .getActionTypes()) {
-      final ApprovalCategory c = at.getCategory();
-      if (Gerrit.getConfig().getWildProject().equals(projectKey)
-          && !c.getId().canBeOnWildProject()) {
-        // Giving out control of the WILD_PROJECT to other groups beyond
-        // Administrators is dangerous. Having control over WILD_PROJECT
-        // is about the same as having Administrator access as users are
-        // able to affect grants in all projects on the system.
-        //
-        continue;
-      }
-      catBox.addItem(c.getName(), c.getId().get());
-    }
-
-    if (catBox.getItemCount() > 0) {
-      catBox.setSelectedIndex(0);
-      updateCategorySelection();
-    }
-  }
-
-  public void enableForm(final boolean on) {
-    final boolean canAdd = on && catBox.getItemCount() > 0;
-    addBut.setEnabled(canAdd);
-    clearBut.setEnabled(canAdd);
-    nameTxt.setEnabled(canAdd);
-    referenceTxt.setEnabled(canAdd);
-    catBox.setEnabled(canAdd);
-    topBox.setEnabled(canAdd);
-    botBox.setEnabled(canAdd);
-  }
-
-  public void clear() {
-    setCat(null);
-    setName("");
-    setReference("");
-  }
-
-  public void load(final RefRight right, final AccountGroup group) {
-    final ApprovalType atype =
-       Gerrit.getConfig().getApprovalTypes().getApprovalType(
-          right.getApprovalCategoryId());
-
-    setCat(atype != null ? atype.getCategory().getName()
-                         : right.getApprovalCategoryId().get() );
-
-    setName(group.getName());
-    setReference(right.getRefPatternForDisplay());
-
-    setRange(atype.getCategory().isRange() ? atype.getValue(right.getMinValue())
-             : null, atype.getValue(right.getMaxValue()) );
-  }
-
-  protected void doAddNewRight() {
-    final ApprovalType at = getApprovalType();
-    ApprovalCategoryValue min = getMin(at);
-    ApprovalCategoryValue max = getMax(at);
-
-    if (at == null || min == null || max == null) {
-      return;
-    }
-
-    final String groupName = nameSug.getText();
-    if ("".equals(groupName)
-        || Util.C.defaultAccountGroupName().equals(groupName)) {
-      return;
-    }
-
-    final String refPattern = referenceTxt.getText();
-
-    addBut.setEnabled(false);
-    Util.PROJECT_SVC.addRight(projectKey, at.getCategory().getId(),
-        groupName, refPattern, min.getValue(), max.getValue(),
-        new GerritCallback<ProjectDetail>() {
-          public void onSuccess(final ProjectDetail result) {
-            addBut.setEnabled(true);
-            nameSug.setText("");
-            referenceTxt.setText("");
-            ValueChangeEvent.fire(AccessRightEditor.this, result);
-          }
-
-          @Override
-          public void onFailure(final Throwable caught) {
-            addBut.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  protected void updateCategorySelection() {
-    final ApprovalType at = getApprovalType();
-
-    if (at == null || at.getValues().isEmpty()) {
-      topBox.setEnabled(false);
-      botBox.setEnabled(false);
-      referenceTxt.setEnabled(false);
-      addBut.setEnabled(false);
-      clearBut.setEnabled(false);
-      return;
-    }
-
-    updateRanges(at);
-  }
-
-  protected void updateRanges(final ApprovalType at) {
-    ApprovalCategoryValue min = null, max = null, last = null;
-
-    topBox.clear();
-    botBox.clear();
-
-    for(final ApprovalCategoryValue v : at.getValues()) {
-      final int nval = v.getValue();
-      final String vStr = String.valueOf(nval);
-
-      String nStr = vStr + ": " + v.getName();
-      if (nval > 0) {
-        nStr = "+" + nStr;
-      }
-
-      topBox.addItem(nStr, vStr);
-      botBox.addItem(nStr, vStr);
-
-      if (min == null || nval < 0) {
-        min = v;
-      } else if (max == null && nval > 0) {
-        max = v;
-      }
-      last = v;
-    }
-
-    if (max == null) {
-      max = last;
-    }
-
-    if (ApprovalCategory.READ.equals(at.getCategory().getId())) {
-      // Special case; for READ the most logical range is just
-      // +1 READ, so assume that as the default for both.
-      min = max;
-    }
-
-    if (! at.getCategory().isRange()) {
-      max = null;
-    }
-
-    setRange(min, max);
-  }
-
-  protected void setCat(final String cat) {
-    if (cat == null) {
-      catBox.setSelectedIndex(0);
-    } else {
-      setSelectedText(catBox, cat);
-    }
-    updateCategorySelection();
-  }
-
-  protected void setName(final String name) {
-    nameTxt.setText(name);
-  }
-
-  protected void setReference(final String ref) {
-    referenceTxt.setText(ref);
-  }
-
-  protected void setRange(final ApprovalCategoryValue min,
-                          final ApprovalCategoryValue max) {
-    if (min == null || max == null) {
-      botBox.setVisible(false);
-      if (max != null) {
-        setSelectedValue(topBox, "" + max.getValue());
-        return;
-      }
-    } else {
-      botBox.setVisible(true);
-      setSelectedValue(botBox, "" + max.getValue());
-    }
-    setSelectedValue(topBox, "" + min.getValue());
-  }
-
-  private ApprovalType getApprovalType() {
-    int idx = catBox.getSelectedIndex();
-    if (idx < 0) {
-      return null;
-    }
-    return Gerrit.getConfig().getApprovalTypes().getApprovalType(
-             new ApprovalCategory.Id(catBox.getValue(idx)));
-  }
-
-  public ApprovalCategoryValue getMin(ApprovalType at) {
-    final ApprovalCategoryValue top = getTop(at);
-    final ApprovalCategoryValue bot = getBot(at);
-    if (bot == null) {
-      for (final ApprovalCategoryValue v : at.getValues()) {
-        if (0 <= v.getValue() && v.getValue() <= top.getValue()) {
-          return v;
-        }
-      }
-      return at.getMin();
-    }
-
-    if (top.getValue() > bot.getValue()) {
-      return bot;
-    }
-    return top;
-  }
-
-  public ApprovalCategoryValue getMax(ApprovalType at) {
-    final ApprovalCategoryValue top = getTop(at);
-    final ApprovalCategoryValue bot = getBot(at);
-    if (bot == null || bot.getValue() < top.getValue()) {
-      return top;
-    }
-    return bot;
-  }
-
-  protected ApprovalCategoryValue getTop(ApprovalType at) {
-    int idx = topBox.getSelectedIndex();
-    if (idx < 0) {
-      return null;
-    }
-    return at.getValue(Short.parseShort(topBox.getValue(idx)));
-  }
-
-  protected ApprovalCategoryValue getBot(ApprovalType at) {
-    int idx = botBox.getSelectedIndex();
-    if (idx < 0 || ! botBox.isVisible()) {
-      return null;
-    }
-    return at.getValue(Short.parseShort(botBox.getValue(idx)));
-  }
-
-  public HandlerRegistration addValueChangeHandler(
-      final ValueChangeHandler<ProjectDetail> handler) {
-    return addHandler(handler, ValueChangeEvent.getType());
-  }
-
-  public static boolean setSelectedText(ListBox box, String text) {
-    if (text == null) {
-      return false;
-    }
-    for (int i =0 ; i < box.getItemCount(); i++) {
-      if (text.equals(box.getItemText(i))) {
-        box.setSelectedIndex(i);
-        return true;
-      }
-    }
-    return false;
-  }
-
-  public static boolean setSelectedValue(ListBox box, String value) {
-    if (value == null) {
-      return false;
-    }
-    for (int i =0 ; i < box.getItemCount(); i++) {
-      if (value.equals(box.getValue(i))) {
-        box.setSelectedIndex(i);
-        return true;
-      }
-    }
-    return false;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
new file mode 100644
index 0000000..fdcaa00
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
@@ -0,0 +1,238 @@
+// 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.client.admin;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.ProjectAccess;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.SpanElement;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.editor.client.Editor;
+import com.google.gwt.editor.client.EditorDelegate;
+import com.google.gwt.editor.client.ValueAwareEditor;
+import com.google.gwt.editor.client.adapters.EditorSource;
+import com.google.gwt.editor.client.adapters.ListEditor;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.MouseOutEvent;
+import com.google.gwt.event.dom.client.MouseOverEvent;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.ValueListBox;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AccessSectionEditor extends Composite implements
+    Editor<AccessSection>, ValueAwareEditor<AccessSection> {
+  interface Binder extends UiBinder<HTMLPanel, AccessSectionEditor> {
+  }
+
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField
+  ValueEditor<String> refPattern;
+
+  @UiField
+  FlowPanel permissionContainer;
+  ListEditor<Permission, PermissionEditor> permissions;
+
+  @UiField
+  DivElement addContainer;
+  @UiField(provided = true)
+  @Editor.Ignore
+  ValueListBox<String> permissionSelector;
+
+  @UiField
+  SpanElement deletedName;
+
+  @UiField
+  Anchor deleteSection;
+
+  @UiField
+  DivElement normal;
+  @UiField
+  DivElement deleted;
+
+  private final ProjectAccess projectAccess;
+  private AccessSection value;
+  private boolean editing;
+  private boolean readOnly;
+  private boolean isDeleted;
+
+  public AccessSectionEditor(ProjectAccess access) {
+    projectAccess = access;
+
+    permissionSelector =
+        new ValueListBox<String>(PermissionNameRenderer.INSTANCE);
+    permissionSelector.addValueChangeHandler(new ValueChangeHandler<String>() {
+      @Override
+      public void onValueChange(ValueChangeEvent<String> event) {
+        if (!Util.C.addPermission().equals(event.getValue())) {
+          onAddPermission(event.getValue());
+        }
+      }
+    });
+
+    initWidget(uiBinder.createAndBindUi(this));
+    permissions = ListEditor.of(new PermissionEditorSource());
+  }
+
+  @UiHandler("deleteSection")
+  void onDeleteHover(MouseOverEvent event) {
+    normal.addClassName(AdminResources.I.css().deleteSectionHover());
+  }
+
+  @UiHandler("deleteSection")
+  void onDeleteNonHover(MouseOutEvent event) {
+    normal.removeClassName(AdminResources.I.css().deleteSectionHover());
+  }
+
+  @UiHandler("deleteSection")
+  void onDeleteSection(ClickEvent event) {
+    isDeleted = true;
+    deletedName.setInnerText(refPattern.getValue());
+    normal.getStyle().setDisplay(Display.NONE);
+    deleted.getStyle().setDisplay(Display.BLOCK);
+  }
+
+  @UiHandler("undoDelete")
+  void onUndoDelete(ClickEvent event) {
+    isDeleted = false;
+    deleted.getStyle().setDisplay(Display.NONE);
+    normal.getStyle().setDisplay(Display.BLOCK);
+  }
+
+  void onAddPermission(String varName) {
+    Permission p = value.getPermission(varName, true);
+    permissions.getList().add(p);
+    rebuildPermissionSelector();
+  }
+
+  void editRefPattern() {
+    refPattern.edit();
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      @Override
+      public void execute() {
+        refPattern.setFocus(true);
+      }});
+  }
+
+  void enableEditing() {
+    readOnly = false;
+    addContainer.getStyle().setDisplay(Display.BLOCK);
+    rebuildPermissionSelector();
+  }
+
+  boolean isDeleted() {
+    return isDeleted;
+  }
+
+  @Override
+  public void setValue(AccessSection value) {
+    this.value = value;
+    this.readOnly = !editing || !projectAccess.isOwnerOf(value);
+
+    refPattern.setEnabled(!readOnly);
+    deleteSection.setVisible(!readOnly);
+
+    if (readOnly) {
+      addContainer.getStyle().setDisplay(Display.NONE);
+    } else {
+      enableEditing();
+    }
+  }
+
+  void setEditing(final boolean editing) {
+    this.editing = editing;
+  }
+
+  private void rebuildPermissionSelector() {
+    List<String> perms = new ArrayList<String>();
+    for (ApprovalType t : Gerrit.getConfig().getApprovalTypes()
+        .getApprovalTypes()) {
+      String varName = Permission.LABEL + t.getCategory().getLabelName();
+      if (value.getPermission(varName) == null) {
+        perms.add(varName);
+      }
+    }
+    for (String varName : Util.C.permissionNames().keySet()) {
+      if (value.getPermission(varName) == null) {
+        perms.add(varName);
+      }
+    }
+    if (perms.isEmpty()) {
+      addContainer.getStyle().setDisplay(Display.NONE);
+    } else {
+      addContainer.getStyle().setDisplay(Display.BLOCK);
+      perms.add(0, Util.C.addPermission());
+      permissionSelector.setValue(Util.C.addPermission());
+      permissionSelector.setAcceptableValues(perms);
+    }
+  }
+
+  @Override
+  public void flush() {
+    List<Permission> src = permissions.getList();
+    List<Permission> keep = new ArrayList<Permission>(src.size());
+
+    for (int i = 0; i < src.size(); i++) {
+      PermissionEditor e = (PermissionEditor) permissionContainer.getWidget(i);
+      if (!e.isDeleted()) {
+        keep.add(src.get(i));
+      }
+    }
+    value.setPermissions(keep);
+  }
+
+  @Override
+  public void onPropertyChange(String... paths) {
+  }
+
+  @Override
+  public void setDelegate(EditorDelegate<AccessSection> delegate) {
+  }
+
+  private class PermissionEditorSource extends EditorSource<PermissionEditor> {
+    @Override
+    public PermissionEditor create(int index) {
+      PermissionEditor subEditor = new PermissionEditor(readOnly, value);
+      permissionContainer.insert(subEditor, index);
+      return subEditor;
+    }
+
+    @Override
+    public void dispose(PermissionEditor subEditor) {
+      subEditor.removeFromParent();
+    }
+
+    @Override
+    public void setIndex(PermissionEditor subEditor, int index) {
+      permissionContainer.insert(subEditor, index);
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml
new file mode 100644
index 0000000..fdae4ed
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml
@@ -0,0 +1,157 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:e='urn:import:com.google.gwt.editor.ui.client'
+  xmlns:my='urn:import:com.google.gerrit.client.admin'
+  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
+  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
+  ui:generateLocales='default,en'
+  >
+<ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
+<ui:style>
+  @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+  @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+
+  .panel {
+    position: relative;
+  }
+
+  .content {
+    margin-top: 4px;
+    margin-bottom: 4px;
+    padding-bottom: 2px;
+  }
+
+  .normal {
+    background-color: trimColor;
+  }
+
+  .deleted {
+    padding-left: 7px;
+    padding-bottom: 2px;
+  }
+
+  .header {
+    padding-left: 5px;
+    padding-right: 5px;
+  }
+  .headerText {
+    vertical-align: top;
+    white-space: nowrap;
+    font-weight: bold;
+  }
+  .headerTable {
+    border: 0;
+    width: 100%;
+    padding-right: 40px;
+  }
+
+  .header:hover {
+    background-color: selectionColor;
+  }
+
+  .refName {
+    width: 100%;
+  }
+  .refNameEdit {
+    width: 100%;
+  }
+
+  .permissionList {
+    margin-left: 5px;
+    margin-right: 5px;
+  }
+
+  .addContainer {
+    padding-left: 16px;
+    padding-right: 16px;
+    font-size: 80%;
+  }
+  .addContainer:hover {
+    background-color: selectionColor;
+  }
+  .addSelector {
+    font-size: 80%;
+  }
+
+  .deleteIcon {
+    position: absolute;
+    top: 5px;
+    right: 17px;
+  }
+
+  .undoIcon {
+    position: absolute;
+    top: 2px;
+    right: 17px;
+  }
+</ui:style>
+
+<g:HTMLPanel styleName='{style.panel}'>
+<div ui:field='normal' class='{style.normal} {style.content}'>
+  <div class='{style.header}'>
+    <table class='{style.headerTable}'><tr>
+      <td class='{style.headerText}'><ui:msg>Reference:</ui:msg></td>
+      <td width='100%'>
+        <my:ValueEditor
+            ui:field='refPattern'
+            addStyleNames='{style.refName}'
+            editTitle='Edit reference pattern'>
+          <ui:attribute name='editTitle'/>
+          <my:editor>
+            <my:RefPatternBox styleName='{style.refNameEdit}'/>
+          </my:editor>
+        </my:ValueEditor>
+      </td>
+    </tr></table>
+
+    <g:Anchor
+        ui:field='deleteSection'
+        href='javascript:void'
+        styleName='{style.deleteIcon} {res.css.deleteIcon}'
+        title='Delete this section (and nested rules)'>
+      <ui:attribute name='title'/>
+    </g:Anchor>
+  </div>
+
+  <g:FlowPanel
+      ui:field='permissionContainer'
+      styleName='{style.permissionList}'/>
+  <div ui:field='addContainer' class='{style.addContainer}'>
+    <g:ValueListBox
+        ui:field='permissionSelector'
+        styleName='{style.addSelector}' />
+  </div>
+</div>
+
+<div
+    ui:field='deleted'
+    class='{style.deleted} {res.css.deleted}'
+    style='display: none'>
+  <ui:msg>Reference <span ui:field='deletedName'/> was deleted</ui:msg>
+  <g:Anchor
+      ui:field='undoDelete'
+      href='javascript:void'
+      styleName='{style.undoIcon} {res.css.undoIcon}'
+      title='Undo deletion'>
+    <ui:attribute name='title'/>
+  </g:Anchor>
+</div>
+</g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
index 593ab34..9f8117a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
@@ -63,7 +63,9 @@
 import java.util.List;
 
 public class AccountGroupScreen extends AccountScreen {
-  private final AccountGroup.Id groupId;
+  private AccountGroup.Id groupId;
+  private AccountGroup.UUID groupUUID;
+
   private AccountInfoCache accounts = AccountInfoCache.empty();
   private GroupInfoCache groups = GroupInfoCache.empty();
   private MemberTable members;
@@ -106,24 +108,31 @@
     groupId = toShow;
   }
 
+  public AccountGroupScreen(final AccountGroup.UUID toShow) {
+    groupUUID = toShow;
+  }
+
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.GROUP_SVC.groupDetail(groupId, new ScreenLoadCallback<GroupDetail>(
-        this) {
-      @Override
-      protected void preDisplay(final GroupDetail result) {
-        enableForm(result.canModify);
-        saveName.setVisible(result.canModify);
-        saveOwner.setVisible(result.canModify);
-        saveDesc.setVisible(result.canModify);
-        saveGroupOptions.setVisible(result.canModify);
-        delMember.setVisible(result.canModify);
-        saveType.setVisible(result.canModify);
-        delInclude.setVisible(result.canModify);
-        display(result);
-      }
-    });
+    Util.GROUP_SVC.groupDetail(groupId, groupUUID,
+        new ScreenLoadCallback<GroupDetail>(this) {
+          @Override
+          protected void preDisplay(final GroupDetail result) {
+            groupId = result.group.getId();
+            groupUUID = result.group.getGroupUUID();
+            display(result);
+
+            enableForm(result.canModify);
+            saveName.setVisible(result.canModify);
+            saveOwner.setVisible(result.canModify);
+            saveDesc.setVisible(result.canModify);
+            saveGroupOptions.setVisible(result.canModify);
+            delMember.setVisible(result.canModify);
+            saveType.setVisible(result.canModify);
+            delInclude.setVisible(result.canModify);
+          }
+        });
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
index 4631a29..4f95a70 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.java
@@ -16,6 +16,8 @@
 
 import com.google.gwt.i18n.client.Constants;
 
+import java.util.Map;
+
 public interface AdminConstants extends Constants {
   String defaultAccountName();
   String defaultAccountGroupName();
@@ -101,4 +103,14 @@
   String noGroupSelected();
   String errorNoMatchingGroups();
   String errorNoGitRepository();
+
+  String addPermission();
+  Map<String,String> permissionNames();
+
+  String refErrorEmpty();
+  String refErrorBeginSlash();
+  String refErrorDoubleSlash();
+  String refErrorNoSpace();
+  String refErrorPrintable();
+  String errorsMustBeFixed();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
index 4bad368..e9575cf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminConstants.properties
@@ -83,3 +83,36 @@
 noGroupSelected = (No group selected)
 errorNoMatchingGroups = No Matching Groups
 errorNoGitRepository = No Git Repository
+
+
+addPermission = Add Permission ...
+
+# Permission Names
+permissionNames = \
+	create, \
+	forgeAuthor, \
+	forgeCommitter, \
+	forgeServerAsCommitter, \
+	owner, \
+	push, \
+	pushMerge, \
+	pushTag, \
+	read, \
+	submit
+create = Create Reference
+forgeAuthor = Forge Author Identity
+forgeCommitter = Forge Committer Identity
+forgeServerAsCommitter = Forge Server Identity
+owner = Owner
+push = Push
+pushMerge = Push Merge Commit
+pushTag = Push Annotated Tag
+read = Read
+submit = Submit
+
+refErrorEmpty = Reference must be supplied
+refErrorBeginSlash = Reference must not start with '/'
+refErrorDoubleSlash = References cannot contain '//'
+refErrorNoSpace = References cannot contain spaces
+refErrorPrintable = References may contain only printable characters
+errorsMustBeFixed = Errors must be fixed before committing changes.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminCss.java
similarity index 69%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java
copy to gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminCss.java
index 0977ee9..55a9bf3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminCss.java
@@ -12,15 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.client.admin;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.gwt.resources.client.CssResource;
 
-public class Schema_49 extends SchemaVersion {
+public interface AdminCss extends CssResource {
+  String deleteIcon();
+  String undoIcon();
 
-  @Inject
-  Schema_49(Provider<Schema_48> prior) {
-    super(prior);
-  }
+  String deleted();
+  String deletedBorder();
+
+  String deleteSectionHover();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
index ab3541e..9ce3ccf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
@@ -18,6 +18,7 @@
 
 public interface AdminMessages extends Messages {
   String group(String name);
+  String label(String name);
   String project(String name);
   String deletedGroup(int id);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
index 6feb69a..60d9c70 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
@@ -1,3 +1,4 @@
 group = Group {0}
+label = Label {0}
 project = Project {0}
 deletedGroup = Deleted Group {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
new file mode 100644
index 0000000..cd366f3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
@@ -0,0 +1,38 @@
+// 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.client.admin;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.ImageResource;
+
+public interface AdminResources extends ClientBundle {
+  public static final AdminResources I = GWT.create(AdminResources.class);
+
+  @Source("admin.css")
+  AdminCss css();
+
+  @Source("editText.png")
+  public ImageResource editText();
+
+  @Source("deleteNormal.png")
+  public ImageResource deleteNormal();
+
+  @Source("deleteHover.png")
+  public ImageResource deleteHover();
+
+  @Source("undoNormal.png")
+  public ImageResource undoNormal();
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
new file mode 100644
index 0000000..b51f0c0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
@@ -0,0 +1,143 @@
+// 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.client.admin;
+
+import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
+import com.google.gerrit.client.ui.RPCSuggestOracle;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gwt.editor.client.LeafValueEditor;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.event.logical.shared.HasCloseHandlers;
+import com.google.gwt.event.logical.shared.HasSelectionHandlers;
+import com.google.gwt.event.logical.shared.SelectionEvent;
+import com.google.gwt.event.logical.shared.SelectionHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Focusable;
+import com.google.gwt.user.client.ui.SuggestBox;
+import com.google.gwt.user.client.ui.SuggestBox.DefaultSuggestionDisplay;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+
+public class GroupReferenceBox extends Composite implements
+    LeafValueEditor<GroupReference>, HasSelectionHandlers<GroupReference>,
+    HasCloseHandlers<GroupReferenceBox>, Focusable {
+  private final DefaultSuggestionDisplay suggestions;
+  private final NpTextBox textBox;
+  private final AccountGroupSuggestOracle oracle;
+  private final SuggestBox suggestBox;
+
+  private boolean submitOnSelection;
+
+  public GroupReferenceBox() {
+    suggestions = new DefaultSuggestionDisplay();
+    textBox = new NpTextBox();
+    oracle = new AccountGroupSuggestOracle();
+    suggestBox = new SuggestBox( //
+        new RPCSuggestOracle(oracle), //
+        textBox, //
+        suggestions);
+    initWidget(suggestBox);
+
+    suggestBox.addKeyPressHandler(new KeyPressHandler() {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        submitOnSelection = false;
+
+        if (event.getCharCode() == KeyCodes.KEY_ENTER) {
+          if (suggestions.isSuggestionListShowing()) {
+            submitOnSelection = true;
+          } else {
+            SelectionEvent.fire(GroupReferenceBox.this, getValue());
+          }
+        }
+      }
+    });
+    suggestBox.addKeyUpHandler(new KeyUpHandler() {
+      @Override
+      public void onKeyUp(KeyUpEvent event) {
+        if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
+          suggestBox.setText("");
+          CloseEvent.fire(GroupReferenceBox.this, GroupReferenceBox.this);
+        }
+      }
+    });
+    suggestBox.addSelectionHandler(new SelectionHandler<Suggestion>() {
+      @Override
+      public void onSelection(SelectionEvent<Suggestion> event) {
+        if (submitOnSelection) {
+          submitOnSelection = false;
+          SelectionEvent.fire(GroupReferenceBox.this, getValue());
+        }
+      }
+    });
+  }
+
+  public void setVisibleLength(int len) {
+    textBox.setVisibleLength(len);
+  }
+
+  @Override
+  public HandlerRegistration addSelectionHandler(
+      SelectionHandler<GroupReference> handler) {
+    return addHandler(handler, SelectionEvent.getType());
+  }
+
+  @Override
+  public HandlerRegistration addCloseHandler(
+      CloseHandler<GroupReferenceBox> handler) {
+    return addHandler(handler, CloseEvent.getType());
+  }
+
+  @Override
+  public GroupReference getValue() {
+    String name = suggestBox.getText();
+    if (name != null && !name.isEmpty()) {
+      return new GroupReference(oracle.getUUID(name), name);
+    } else {
+      return null;
+    }
+  }
+
+  @Override
+  public void setValue(GroupReference value) {
+    suggestBox.setText(value != null ? value.getName() : "");
+  }
+
+  @Override
+  public int getTabIndex() {
+    return suggestBox.getTabIndex();
+  }
+
+  @Override
+  public void setTabIndex(int index) {
+    suggestBox.setTabIndex(index);
+  }
+
+  public void setFocus(boolean focused) {
+    suggestBox.setFocus(focused);
+  }
+
+  @Override
+  public void setAccessKey(char key) {
+    suggestBox.setAccessKey(key);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
new file mode 100644
index 0000000..c21844c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
@@ -0,0 +1,297 @@
+// 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.client.admin;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.SuggestUtil;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.editor.client.Editor;
+import com.google.gwt.editor.client.EditorDelegate;
+import com.google.gwt.editor.client.ValueAwareEditor;
+import com.google.gwt.editor.client.adapters.EditorSource;
+import com.google.gwt.editor.client.adapters.ListEditor;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.MouseOutEvent;
+import com.google.gwt.event.dom.client.MouseOverEvent;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.SelectionEvent;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.ValueLabel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PermissionEditor extends Composite implements Editor<Permission>,
+    ValueAwareEditor<Permission> {
+  interface Binder extends UiBinder<HTMLPanel, PermissionEditor> {
+  }
+
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField(provided = true)
+  @Path("name")
+  ValueLabel<String> normalName;
+
+  @UiField(provided = true)
+  @Path("name")
+  ValueLabel<String> deletedName;
+
+  @UiField
+  CheckBox exclusiveGroup;
+
+  @UiField
+  FlowPanel ruleContainer;
+  ListEditor<PermissionRule, PermissionRuleEditor> rules;
+
+  @UiField
+  DivElement addContainer;
+  @UiField
+  DivElement addStage1;
+  @UiField
+  DivElement addStage2;
+  @UiField
+  Anchor beginAddRule;
+  @UiField
+  @Editor.Ignore
+  GroupReferenceBox groupToAdd;
+  @UiField
+  Button addRule;
+
+  @UiField
+  Anchor deletePermission;
+
+  @UiField
+  DivElement normal;
+  @UiField
+  DivElement deleted;
+
+  private final boolean readOnly;
+  private final AccessSection section;
+  private Permission value;
+  private ApprovalType rangeType;
+  private boolean isDeleted;
+
+  public PermissionEditor(boolean readOnly, AccessSection section) {
+    this.readOnly = readOnly;
+    this.section = section;
+
+    normalName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
+    deletedName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
+
+    initWidget(uiBinder.createAndBindUi(this));
+    rules = ListEditor.of(new RuleEditorSource());
+
+    exclusiveGroup.setEnabled(!readOnly);
+
+    if (readOnly) {
+      addContainer.removeFromParent();
+      addContainer = null;
+
+      deletePermission.removeFromParent();
+      deletePermission = null;
+    }
+  }
+
+  @UiHandler("deletePermission")
+  void onDeleteHover(MouseOverEvent event) {
+    addStyleName(AdminResources.I.css().deleteSectionHover());
+  }
+
+  @UiHandler("deletePermission")
+  void onDeleteNonHover(MouseOutEvent event) {
+    removeStyleName(AdminResources.I.css().deleteSectionHover());
+  }
+
+  @UiHandler("deletePermission")
+  void onDeletePermission(ClickEvent event) {
+    isDeleted = true;
+    normal.getStyle().setDisplay(Display.NONE);
+    deleted.getStyle().setDisplay(Display.BLOCK);
+  }
+
+  @UiHandler("undoDelete")
+  void onUndoDelete(ClickEvent event) {
+    isDeleted = false;
+    deleted.getStyle().setDisplay(Display.NONE);
+    normal.getStyle().setDisplay(Display.BLOCK);
+  }
+
+  @UiHandler("beginAddRule")
+  void onBeginAddRule(ClickEvent event) {
+    addStage1.getStyle().setDisplay(Display.NONE);
+    addStage2.getStyle().setDisplay(Display.BLOCK);
+
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      @Override
+      public void execute() {
+        groupToAdd.setFocus(true);
+      }
+    });
+  }
+
+  @UiHandler("addRule")
+  void onAddGroupByClick(ClickEvent event) {
+    GroupReference ref = groupToAdd.getValue();
+    if (ref != null) {
+      addGroup(ref);
+    } else {
+      groupToAdd.setFocus(true);
+    }
+  }
+
+  @UiHandler("groupToAdd")
+  void onAddGroupByEnter(SelectionEvent<GroupReference> event) {
+    GroupReference ref = event.getSelectedItem();
+    if (ref != null) {
+      addGroup(ref);
+    }
+  }
+
+  @UiHandler("groupToAdd")
+  void onAbortAddGroup(CloseEvent<GroupReferenceBox> event) {
+    hideAddGroup();
+  }
+
+  @UiHandler("hideAddGroup")
+  void hideAddGroup(ClickEvent event) {
+    hideAddGroup();
+  }
+
+  private void hideAddGroup() {
+    addStage1.getStyle().setDisplay(Display.BLOCK);
+    addStage2.getStyle().setDisplay(Display.NONE);
+  }
+
+  private void addGroup(GroupReference ref) {
+    if (ref.getUUID() != null) {
+      if (value.getRule(ref) == null) {
+        PermissionRule newRule = value.getRule(ref, true);
+        if (rangeType != null) {
+          int min = rangeType.getMin().getValue();
+          int max = rangeType.getMax().getValue();
+          newRule.setRange(min, max);
+        }
+        rules.getList().add(newRule);
+      }
+      groupToAdd.setValue(null);
+      groupToAdd.setFocus(true);
+
+    } else {
+      // If the oracle didn't get to complete a UUID, resolve it now.
+      //
+      addRule.setEnabled(false);
+      SuggestUtil.SVC.suggestAccountGroup(ref.getName(), 1,
+          new GerritCallback<List<GroupReference>>() {
+            @Override
+            public void onSuccess(List<GroupReference> result) {
+              addRule.setEnabled(true);
+              if (result.size() == 1) {
+                addGroup(result.get(0));
+              } else {
+                groupToAdd.setFocus(true);
+              }
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+              addRule.setEnabled(true);
+              super.onFailure(caught);
+            }
+          });
+    }
+  }
+
+  boolean isDeleted() {
+    return isDeleted;
+  }
+
+  @Override
+  public void setValue(Permission value) {
+    this.value = value;
+    if (value.isLabel()) {
+      rangeType =
+          Gerrit.getConfig().getApprovalTypes().byLabel(value.getLabel());
+    } else {
+      rangeType = null;
+    }
+
+    if (value != null && Permission.OWNER.equals(value.getName())) {
+      exclusiveGroup.setEnabled(false);
+    } else {
+      exclusiveGroup.setEnabled(!readOnly);
+    }
+  }
+
+  @Override
+  public void flush() {
+    List<PermissionRule> src = rules.getList();
+    List<PermissionRule> keep = new ArrayList<PermissionRule>(src.size());
+
+    for (int i = 0; i < src.size(); i++) {
+      PermissionRuleEditor e =
+          (PermissionRuleEditor) ruleContainer.getWidget(i);
+      if (!e.isDeleted()) {
+        keep.add(src.get(i));
+      }
+    }
+    value.setRules(keep);
+  }
+
+  @Override
+  public void onPropertyChange(String... paths) {
+  }
+
+  @Override
+  public void setDelegate(EditorDelegate<Permission> delegate) {
+  }
+
+  private class RuleEditorSource extends EditorSource<PermissionRuleEditor> {
+    @Override
+    public PermissionRuleEditor create(int index) {
+      PermissionRuleEditor subEditor =
+          new PermissionRuleEditor(readOnly, section, value, rangeType);
+      ruleContainer.insert(subEditor, index);
+      return subEditor;
+    }
+
+    @Override
+    public void dispose(PermissionRuleEditor subEditor) {
+      subEditor.removeFromParent();
+    }
+
+    @Override
+    public void setIndex(PermissionRuleEditor subEditor, int index) {
+      ruleContainer.insert(subEditor, index);
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml
new file mode 100644
index 0000000..25995d9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:e='urn:import:com.google.gwt.editor.ui.client'
+  xmlns:my='urn:import:com.google.gerrit.client.admin'
+  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
+  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
+  ui:generateLocales='default,en'
+  >
+<ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
+<ui:style>
+  @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+  @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
+
+  .panel {
+    position: relative;
+  }
+
+  .normal {
+    border: 1px solid backgroundColor;
+    margin-top: -1px;
+    margin-bottom: -1px;
+  }
+
+  .header {
+    padding-left: 5px;
+    padding-right: 5px;
+    padding-bottom: 1px;
+    white-space: nowrap;
+  }
+
+  .header:hover {
+    background-color: selectionColor;
+  }
+
+  .name {
+    font-style: italic;
+  }
+
+  .exclusiveGroup {
+    position: absolute;
+    top: 0;
+    right: 36px;
+    width: 7em;
+    font-size: 80%;
+  }
+
+  .addContainer {
+    padding-left: 10px;
+    position: relative;
+  }
+  .addContainer:hover {
+    background-color: selectionColor;
+  }
+  .addLink {
+    font-size: 80%;
+  }
+
+  .deleteIcon {
+    position: absolute;
+    top: 1px;
+    right: 12px;
+  }
+</ui:style>
+
+<g:HTMLPanel stylePrimaryName='{style.panel}'>
+<div ui:field='normal' class='{style.normal}'>
+  <div class='{style.header}'>
+    <g:ValueLabel styleName='{style.name}' ui:field='normalName'/>
+    <g:CheckBox
+        ui:field='exclusiveGroup'
+        addStyleNames='{style.exclusiveGroup}'
+        text='Exclusive'>
+      <ui:attribute name='text'/>
+    </g:CheckBox>
+  <g:Anchor
+      ui:field='deletePermission'
+      href='javascript:void'
+      styleName='{style.deleteIcon} {res.css.deleteIcon}'
+      title='Delete this permission (and nested rules)'>
+    <ui:attribute name='title'/>
+  </g:Anchor>
+  </div>
+  <g:FlowPanel ui:field='ruleContainer'/>
+  <div ui:field='addContainer' class='{style.addContainer}'>
+    <div ui:field='addStage1'>
+      <g:Anchor
+          ui:field='beginAddRule'
+          styleName='{style.addLink}'
+          href='javascript:void'
+          text='Add Group'>
+        <ui:attribute name='text'/>
+      </g:Anchor>
+    </div>
+    <div ui:field='addStage2' style='display: none'>
+      <ui:msg>Group Name: <my:GroupReferenceBox
+                                            ui:field='groupToAdd'
+                                            visibleLength='45'/></ui:msg>
+      <g:Button
+          ui:field='addRule'
+          text='Add'>
+        <ui:attribute name='text'/>
+      </g:Button>
+      <g:Anchor
+          ui:field='hideAddGroup'
+          href='javascript:void'
+          styleName='{style.deleteIcon} {res.css.deleteIcon}'
+          title='Cancel additional group'>
+        <ui:attribute name='title'/>
+      </g:Anchor>
+    </div>
+  </div>
+</div>
+
+<div
+    ui:field='deleted'
+    class='{res.css.deleted} {res.css.deletedBorder}'
+    style='display: none'>
+  <ui:msg>Permission <g:ValueLabel styleName='{style.name}' ui:field='deletedName'/> was deleted</ui:msg>
+  <g:Anchor
+      ui:field='undoDelete'
+      href='javascript:void'
+      styleName='{style.deleteIcon} {res.css.undoIcon}'
+      title='Undo deletion'>
+    <ui:attribute name='title'/>
+  </g:Anchor>
+</div>
+</g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java
new file mode 100644
index 0000000..1dea1fb
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java
@@ -0,0 +1,53 @@
+// 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.client.admin;
+
+import com.google.gerrit.common.data.Permission;
+import com.google.gwt.text.shared.Renderer;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+class PermissionNameRenderer implements Renderer<String> {
+  static final PermissionNameRenderer INSTANCE = new PermissionNameRenderer();
+
+  private static Map<String, String> LC;
+
+  @Override
+  public String render(String varName) {
+    if (Permission.isLabel(varName)) {
+      return Util.M.label(new Permission(varName).getLabel());
+    }
+
+    Map<String, String> m = Util.C.permissionNames();
+    String desc = m.get(varName);
+    if (desc == null) {
+      if (LC == null) {
+        LC = new HashMap<String, String>();
+        for (Map.Entry<String, String> e : m.entrySet()) {
+          LC.put(e.getKey().toLowerCase(), e.getValue());
+        }
+      }
+      desc = LC.get(varName.toLowerCase());
+    }
+    return desc != null ? desc : varName;
+  }
+
+  @Override
+  public void render(String object, Appendable appendable) throws IOException {
+    appendable.append(render(object));
+  }
+}
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
new file mode 100644
index 0000000..12b71c985
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
@@ -0,0 +1,200 @@
+// 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.client.admin;
+
+import static com.google.gerrit.common.data.Permission.PUSH;
+import static com.google.gerrit.common.data.Permission.PUSH_TAG;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.SpanElement;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.editor.client.Editor;
+import com.google.gwt.editor.client.EditorDelegate;
+import com.google.gwt.editor.client.ValueAwareEditor;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.text.shared.Renderer;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.ValueListBox;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+public class PermissionRuleEditor extends Composite implements
+    Editor<PermissionRule>, ValueAwareEditor<PermissionRule> {
+  interface Binder extends UiBinder<HTMLPanel, PermissionRuleEditor> {
+  }
+
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField(provided = true)
+  ValueListBox<PermissionRule.Action> action;
+
+  @UiField(provided = true)
+  ValueListBox<Integer> min;
+
+  @UiField(provided = true)
+  ValueListBox<Integer> max;
+
+  @UiField
+  CheckBox force;
+
+  @UiField
+  Hyperlink normalGroupName;
+  @UiField
+  SpanElement deletedGroupName;
+
+  @UiField
+  Anchor deleteRule;
+
+  @UiField
+  DivElement normal;
+  @UiField
+  DivElement deleted;
+
+  @UiField
+  SpanElement rangeEditor;
+
+  private boolean isDeleted;
+
+  public PermissionRuleEditor(boolean readOnly, AccessSection section,
+      Permission permission, ApprovalType labelRange) {
+    action = new ValueListBox<PermissionRule.Action>(actionRenderer);
+    min = new ValueListBox<Integer>(rangeRenderer);
+    max = new ValueListBox<Integer>(rangeRenderer);
+
+    if (labelRange != null){
+      min.setValue((int) labelRange.getMin().getValue());
+      max.setValue((int) labelRange.getMax().getValue());
+
+      min.setAcceptableValues(labelRange.getValuesAsList());
+      max.setAcceptableValues(labelRange.getValuesAsList());
+    } else {
+      action.setValue(PermissionRule.Action.ALLOW);
+      action.setAcceptableValues(Arrays.asList(PermissionRule.Action.values()));
+    }
+
+    initWidget(uiBinder.createAndBindUi(this));
+
+    String name = permission.getName();
+    boolean canForce = PUSH.equals(name) || PUSH_TAG.equals(name);
+    if (canForce) {
+      String ref = section.getRefPattern();
+      canForce = !ref.startsWith("refs/for/") && !ref.startsWith("^refs/for/");
+    }
+    force.setVisible(canForce);
+    force.setEnabled(!readOnly);
+
+    if (labelRange != null) {
+      action.getElement().getStyle().setDisplay(Display.NONE);
+      DOM.setElementPropertyBoolean(min.getElement(), "disabled", readOnly);
+      DOM.setElementPropertyBoolean(max.getElement(), "disabled", readOnly);
+    } else {
+      rangeEditor.getStyle().setDisplay(Display.NONE);
+      DOM.setElementPropertyBoolean(action.getElement(), "disabled", readOnly);
+    }
+
+    if (readOnly) {
+      deleteRule.removeFromParent();
+      deleteRule = null;
+    }
+  }
+
+  boolean isDeleted() {
+    return isDeleted;
+  }
+
+  @UiHandler("deleteRule")
+  void onDeleteRule(ClickEvent event) {
+    isDeleted = true;
+    normal.getStyle().setDisplay(Display.NONE);
+    deleted.getStyle().setDisplay(Display.BLOCK);
+  }
+
+  @UiHandler("undoDelete")
+  void onUndoDelete(ClickEvent event) {
+    isDeleted = false;
+    deleted.getStyle().setDisplay(Display.NONE);
+    normal.getStyle().setDisplay(Display.BLOCK);
+  }
+
+  @Override
+  public void setValue(PermissionRule value) {
+    GroupReference ref = value.getGroup();
+    normalGroupName.setTargetHistoryToken(Dispatcher.toGroup(ref.getUUID()));
+    normalGroupName.setText(ref.getName());
+    deletedGroupName.setInnerText(ref.getName());
+  }
+
+  @Override
+  public void setDelegate(EditorDelegate<PermissionRule> delegate) {
+  }
+
+  @Override
+  public void flush() {
+  }
+
+  @Override
+  public void onPropertyChange(String... paths) {
+  }
+
+  private static class ActionRenderer implements
+      Renderer<PermissionRule.Action> {
+    @Override
+    public String render(PermissionRule.Action object) {
+      return object != null ? object.toString() : "";
+    }
+
+    @Override
+    public void render(PermissionRule.Action object, Appendable appendable)
+        throws IOException {
+      appendable.append(render(object));
+    }
+  }
+
+  private static class RangeRenderer implements Renderer<Integer> {
+    @Override
+    public String render(Integer object) {
+      if (0 <= object) {
+        return "+" + object;
+      } else {
+        return String.valueOf(object);
+      }
+    }
+
+    @Override
+    public void render(Integer object, Appendable appendable)
+        throws IOException {
+      appendable.append(render(object));
+    }
+  }
+
+  private static final ActionRenderer actionRenderer = new ActionRenderer();
+  private static final RangeRenderer rangeRenderer = new RangeRenderer();
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml
new file mode 100644
index 0000000..447c99f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:e='urn:import:com.google.gwt.editor.ui.client'
+  xmlns:my='urn:import:com.google.gerrit.client.admin'
+  xmlns:q='urn:import:com.google.gerrit.client.ui'
+  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
+  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
+  ui:generateLocales='default,en'
+  >
+<ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
+<ui:style>
+  @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+
+  .panel {
+    position: relative;
+    height: 1.5em;
+  }
+
+  .panel:hover {
+    background-color: selectionColor;
+  }
+
+  .normal {
+    padding-left: 10px;
+    white-space: nowrap;
+    height: 100%;
+  }
+
+  .deleted {
+    height: 100%;
+  }
+
+  .actionList, .minmax {
+    font-size: 80%;
+  }
+
+  .forcePush {
+    position: absolute;
+    top: 0;
+    right: 36px;
+    width: 7em;
+    font-size: 80%;
+  }
+
+  .deleteIcon {
+    position: absolute;
+    top: 2px;
+    right: 11px;
+  }
+
+  .groupName {
+    display: inline;
+  }
+</ui:style>
+
+<g:HTMLPanel styleName='{style.panel}'>
+<div ui:field='normal' class='{style.normal}'>
+  <g:ValueListBox ui:field='action' styleName='{style.actionList}'/>
+  <span ui:field='rangeEditor'>
+    <g:ValueListBox ui:field='min' styleName='{style.minmax}'/>
+    <g:ValueListBox ui:field='max' styleName='{style.minmax}'/>
+  </span>
+
+  <q:Hyperlink ui:field='normalGroupName' styleName='{style.groupName}'/>
+  <g:CheckBox
+      ui:field='force'
+      addStyleNames='{style.forcePush}'
+      text='Force Push'>
+    <ui:attribute name='text'/>
+  </g:CheckBox>
+
+  <g:Anchor
+      ui:field='deleteRule'
+      href='javascript:void'
+      styleName='{style.deleteIcon} {res.css.deleteIcon}'
+      title='Delete this rule'>
+    <ui:attribute name='title'/>
+  </g:Anchor>
+</div>
+
+<div
+    ui:field='deleted'
+    class='{res.css.deleted} {style.deleted}'
+    style='display: none'>
+  <ui:msg>Group <span ui:field='deletedGroupName'/> was deleted</ui:msg>
+  <g:Anchor
+      ui:field='undoDelete'
+      href='javascript:void'
+      styleName='{style.deleteIcon} {res.css.undoIcon}'
+      title='Undo deletion'>
+    <ui:attribute name='title'/>
+  </g:Anchor>
+</div>
+</g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
new file mode 100644
index 0000000..c550d8c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
@@ -0,0 +1,150 @@
+// 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.client.admin;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ProjectAccess;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.editor.client.Editor;
+import com.google.gwt.editor.client.EditorDelegate;
+import com.google.gwt.editor.client.ValueAwareEditor;
+import com.google.gwt.editor.client.adapters.EditorSource;
+import com.google.gwt.editor.client.adapters.ListEditor;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLPanel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ProjectAccessEditor extends Composite implements
+    Editor<ProjectAccess>, ValueAwareEditor<ProjectAccess> {
+  interface Binder extends UiBinder<HTMLPanel, ProjectAccessEditor> {
+  }
+
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField
+  DivElement inheritsFrom;
+
+  @UiField
+  Hyperlink parentProject;
+
+  @UiField
+  FlowPanel localContainer;
+  ListEditor<AccessSection, AccessSectionEditor> local;
+
+  @UiField
+  Anchor addSection;
+
+  private ProjectAccess value;
+
+  private boolean editing;
+
+  public ProjectAccessEditor() {
+    initWidget(uiBinder.createAndBindUi(this));
+    local = ListEditor.of(new Source(localContainer));
+  }
+
+  @UiHandler("addSection")
+  void onAddSection(ClickEvent event) {
+    int index = local.getList().size();
+    local.getList().add(new AccessSection("refs/heads/*"));
+
+    AccessSectionEditor editor = local.getEditors().get(index);
+    editor.enableEditing();
+    editor.editRefPattern();
+  }
+
+  @Override
+  public void setValue(ProjectAccess value) {
+    this.value = value;
+
+    Project.NameKey parent = value.getInheritsFrom();
+    if (parent != null) {
+      inheritsFrom.getStyle().setDisplay(Display.BLOCK);
+      parentProject.setText(parent.get());
+      parentProject.setTargetHistoryToken( //
+          Dispatcher.toProjectAdmin(parent, ProjectScreen.ACCESS));
+    } else {
+      inheritsFrom.getStyle().setDisplay(Display.NONE);
+    }
+
+    addSection.setVisible(value != null && editing && !value.getOwnerOf().isEmpty());
+  }
+
+  @Override
+  public void flush() {
+    List<AccessSection> src = local.getList();
+    List<AccessSection> keep = new ArrayList<AccessSection>(src.size());
+
+    for (int i = 0; i < src.size(); i++) {
+      AccessSectionEditor e = (AccessSectionEditor) localContainer.getWidget(i);
+      if (!e.isDeleted()) {
+        keep.add(src.get(i));
+      }
+    }
+    value.setLocal(keep);
+  }
+
+  @Override
+  public void onPropertyChange(String... paths) {
+  }
+
+  @Override
+  public void setDelegate(EditorDelegate<ProjectAccess> delegate) {
+  }
+
+  void setEditing(final boolean editing) {
+    this.editing = editing;
+    addSection.setVisible(editing);
+  }
+
+  private class Source extends EditorSource<AccessSectionEditor> {
+    private final FlowPanel container;
+
+    Source(FlowPanel container) {
+      this.container = container;
+    }
+
+    @Override
+    public AccessSectionEditor create(int index) {
+      AccessSectionEditor subEditor = new AccessSectionEditor(value);
+      subEditor.setEditing(editing);
+      container.insert(subEditor, index);
+      return subEditor;
+    }
+
+    @Override
+    public void dispose(AccessSectionEditor subEditor) {
+      subEditor.removeFromParent();
+    }
+
+    @Override
+    public void setIndex(AccessSectionEditor subEditor, int index) {
+      container.insert(subEditor, index);
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
new file mode 100644
index 0000000..9360daa
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:q='urn:import:com.google.gerrit.client.ui'
+  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
+  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
+  ui:generateLocales='default,en'
+  >
+<ui:style>
+  .inheritsFrom {
+    margin-bottom: 0.5em;
+  }
+  .parentTitle {
+    font-weight: bold;
+  }
+  .parentLink {
+    display: inline;
+  }
+
+  .addContainer {
+    margin-top: 5px;
+    font-size: 80%;
+  }
+  .addContainer:hover {
+    background-color: selectionColor;
+  }
+</ui:style>
+
+<g:HTMLPanel>
+  <div ui:field='inheritsFrom' class='{style.inheritsFrom}'>
+    <span class='{style.parentTitle}'><ui:msg>Rights Inherit From:</ui:msg></span>
+    <q:Hyperlink ui:field='parentProject' styleName='{style.parentLink}'/>
+  </div>
+
+  <g:FlowPanel ui:field='localContainer'/>
+  <div class='{style.addContainer}'>
+    <g:Anchor
+        ui:field='addSection'
+        href='javascript:void'
+        text='Add Reference'>
+      <ui:attribute name='text'/>
+    </g:Anchor>
+  </div>
+</g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
index b87f6f1..e9d3889 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2008 The Android Open Source Project
+// 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.
@@ -14,46 +14,60 @@
 
 package com.google.gerrit.client.admin;
 
-import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.FancyFlexTable;
-import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.GerritConfig;
-import com.google.gerrit.common.data.InheritedRefRight;
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.data.ProjectAccess;
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.editor.client.SimpleBeanEditorDriver;
 import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
 
 public class ProjectAccessScreen extends ProjectScreen {
-  private Panel parentPanel;
-  private Hyperlink parentName;
+  interface Binder extends UiBinder<HTMLPanel, ProjectAccessScreen> {
+  }
 
-  private RightsTable rights;
-  private Button delRight;
-  private AccessRightEditor rightEditor;
-  private CheckBox showInherited;
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  interface Driver extends SimpleBeanEditorDriver< //
+      ProjectAccess, //
+      ProjectAccessEditor> {
+  }
+
+  @UiField
+  DivElement editTools;
+
+  @UiField
+  Button edit;
+
+  @UiField
+  Button cancel;
+
+  @UiField
+  ProjectAccessEditor accessEditor;
+
+  @UiField
+  DivElement commitTools;
+
+  @UiField
+  NpTextArea commitMessage;
+
+  @UiField
+  Button commit;
+
+  private Driver driver;
+
+  private ProjectAccess access;
 
   public ProjectAccessScreen(final Project.NameKey toShow) {
     super(toShow);
@@ -62,266 +76,87 @@
   @Override
   protected void onInitUI() {
     super.onInitUI();
-    initParent();
-    initRights();
+    add(uiBinder.createAndBindUi(this));
+
+    driver = GWT.create(Driver.class);
+    accessEditor.setEditing(false);
+    driver.initialize(accessEditor);
   }
 
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.PROJECT_SVC.projectDetail(getProjectKey(),
-        new ScreenLoadCallback<ProjectDetail>(this) {
-          public void preDisplay(final ProjectDetail result) {
-            enableForm(true);
-            display(result);
+    Util.PROJECT_SVC.projectAccess(getProjectKey(),
+        new ScreenLoadCallback<ProjectAccess>(this) {
+          @Override
+          public void preDisplay(ProjectAccess access) {
+            edit(access);
           }
         });
   }
 
-  private void enableForm(final boolean on) {
-    delRight.setEnabled(on);
-    rightEditor.enableForm(on);
+  void edit(ProjectAccess access) {
+    this.access = access;
+    final boolean editing = !edit.isEnabled();
+    accessEditor.setEditing(editing);
+    UIObject.setVisible(editTools, !access.getOwnerOf().isEmpty());
+    cancel.setVisible(editing);
+    UIObject.setVisible(commitTools, editing);
+    driver.edit(access);
   }
 
-  private void initParent() {
-    parentName = new Hyperlink("", "");
-
-    showInherited = new CheckBox();
-    showInherited.setValue(true);
-    showInherited.addClickHandler(new ClickHandler() {
-      public void onClick(ClickEvent event) {
-        rights.showInherited(showInherited.getValue());
-      }
-    });
-
-    Grid g = new Grid(2, 3);
-    g.setWidget(0, 0, new SmallHeading(Util.C.headingParentProjectName()));
-    g.setWidget(1, 0, parentName);
-    g.setWidget(1, 1, showInherited);
-    g.setText(1, 2, Util.C.headingShowInherited());
-
-    parentPanel = new VerticalPanel();
-    parentPanel.add(g);
-    add(parentPanel);
+  @UiHandler("edit")
+  void onEdit(ClickEvent event) {
+    edit.setEnabled(false);
+    cancel.setVisible(true);
+    UIObject.setVisible(commitTools, true);
+    accessEditor.setEditing(true);
+    driver.edit(access);
   }
 
-  private void initRights() {
-    rights = new RightsTable();
-
-    delRight = new Button(Util.C.buttonDeleteGroupMembers());
-    delRight.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        final HashSet<RefRight.Key> refRightIds = rights.getRefRightIdsChecked();
-        doDeleteRefRights(refRightIds);
-      }
-    });
-
-    rightEditor = new AccessRightEditor(getProjectKey());
-    rightEditor.addValueChangeHandler(new ValueChangeHandler<ProjectDetail>() {
-        @Override
-        public void onValueChange(ValueChangeEvent<ProjectDetail> event) {
-          display(event.getValue());
-        }
-      });
-
-    add(new SmallHeading(Util.C.headingAccessRights()));
-    add(rights);
-    add(delRight);
-    add(rightEditor);
+  @UiHandler(value={"cancel", "cancel2"})
+  void onCancel(ClickEvent event) {
+    Gerrit.display(PageLinks.toProjectAcceess(getProjectKey()));
   }
 
-  void display(final ProjectDetail result) {
-    final Project project = result.project;
+  @UiHandler("commit")
+  void onCommit(ClickEvent event) {
+    ProjectAccess access = driver.flush();
 
-    final Project.NameKey wildKey = Gerrit.getConfig().getWildProject();
-    final boolean isWild = wildKey.equals(project.getNameKey());
-    Project.NameKey parent = project.getParent();
-    if (parent == null) {
-      parent = wildKey;
+    if (driver.hasErrors()) {
+      Window.alert(Util.C.errorsMustBeFixed());
+      return;
     }
 
-    parentPanel.setVisible(!isWild);
-    parentName.setTargetHistoryToken(Dispatcher.toProjectAdmin(parent, ACCESS));
-    parentName.setText(parent.get());
+    String message = commitMessage.getText().trim();
+    if ("".equals(message)) {
+      message = null;
+    }
 
-    rights.display(result.groups, result.rights);
+    enable(false);
+    Util.PROJECT_SVC.changeProjectAccess( //
+        getProjectKey(), //
+        access.getRevision(), //
+        message, //
+        access.getLocal(), //
+        new GerritCallback<ProjectAccess>() {
+          @Override
+          public void onSuccess(ProjectAccess access) {
+            commitMessage.setText("");
+            edit(access);
+            enable(true);
+          }
 
-    rightEditor.setVisible(result.canModifyAccess);
-    delRight.setVisible(rights.getCanDelete());
+          @Override
+          public void onFailure(Throwable caught) {
+            enable(true);
+            super.onFailure(caught);
+          }
+        });
   }
 
-  private void doDeleteRefRights(final HashSet<RefRight.Key> refRightIds) {
-    if (!refRightIds.isEmpty()) {
-      Util.PROJECT_SVC.deleteRight(getProjectKey(), refRightIds,
-          new GerritCallback<ProjectDetail>() {
-        @Override
-        public void onSuccess(final ProjectDetail result) {
-          //The user could no longer modify access after deleting a ref right.
-          display(result);
-        }
-      });
-    }
-  }
-
-  private class RightsTable extends FancyFlexTable<InheritedRefRight> {
-    boolean canDelete;
-    Map<AccountGroup.Id, AccountGroup> groups;
-
-    RightsTable() {
-      table.setWidth("");
-      table.setText(0, 2, Util.C.columnRightOrigin());
-      table.setText(0, 3, Util.C.columnApprovalCategory());
-      table.setText(0, 4, Util.C.columnGroupName());
-      table.setText(0, 5, Util.C.columnRefName());
-      table.setText(0, 6, Util.C.columnRightRange());
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
-      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 5, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 6, Gerrit.RESOURCES.css().dataHeader());
-
-      table.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          onOpenRow(table.getCellForEvent(event).getRowIndex());
-        }
-      });
-    }
-
-    HashSet<RefRight.Key> getRefRightIdsChecked() {
-      final HashSet<RefRight.Key> refRightIds = new HashSet<RefRight.Key>();
-      for (int row = 1; row < table.getRowCount(); row++) {
-        RefRight r = getRowItem(row).getRight();
-        if (r != null && table.getWidget(row, 1) instanceof CheckBox
-            && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-          refRightIds.add(r.getKey());
-        }
-      }
-      return refRightIds;
-    }
-
-    void display(final Map<AccountGroup.Id, AccountGroup> grps,
-        final List<InheritedRefRight> refRights) {
-      groups = grps;
-      canDelete = false;
-
-      while (1 < table.getRowCount())
-        table.removeRow(table.getRowCount() - 1);
-
-      for (final InheritedRefRight r : refRights) {
-        final int row = table.getRowCount();
-        table.insertRow(row);
-        if (! showInherited.getValue() && r.isInherited()) {
-          table.getRowFormatter().setVisible(row, false);
-        }
-        applyDataRowStyle(row);
-        populate(row, r);
-      }
-    }
-
-    protected void onOpenRow(final int row) {
-      if (row > 0) {
-        RefRight right = getRowItem(row).getRight();
-        rightEditor.load(right, groups.get(right.getAccountGroupId()));
-      }
-    }
-
-    void populate(final int row, final InheritedRefRight r) {
-      final GerritConfig config = Gerrit.getConfig();
-      final RefRight right = r.getRight();
-      final ApprovalType ar =
-          config.getApprovalTypes().getApprovalType(
-              right.getApprovalCategoryId());
-      final AccountGroup group = groups.get(right.getAccountGroupId());
-
-      if (r.isInherited() || !r.isOwner()) {
-        table.setText(row, 1, "");
-      } else {
-        table.setWidget(row, 1, new CheckBox());
-        canDelete = true;
-      }
-
-      if (r.isInherited()) {
-        Project.NameKey fromProject = right.getKey().getProjectNameKey();
-        table.setWidget(row, 2, new Hyperlink(fromProject.get(), Dispatcher
-            .toProjectAdmin(fromProject, ACCESS)));
-      } else {
-        table.setText(row, 2, "");
-      }
-
-      table.setText(row, 3, ar != null ? ar.getCategory().getName()
-                                       : right.getApprovalCategoryId().get() );
-
-      if (group != null) {
-        table.setWidget(row, 4, new Hyperlink(group.getName(), Dispatcher
-            .toAccountGroup(group.getId())));
-      } else {
-        table.setText(row, 4, Util.M.deletedGroup(right.getAccountGroupId()
-            .get()));
-      }
-
-      table.setText(row, 5, right.getRefPatternForDisplay());
-
-      {
-        final SafeHtmlBuilder m = new SafeHtmlBuilder();
-        final ApprovalCategoryValue min, max;
-        min = ar != null ? ar.getValue(right.getMinValue()) : null;
-        max = ar != null ? ar.getValue(right.getMaxValue()) : null;
-
-        if (ar != null && ar.getCategory().isRange()) {
-          formatValue(m, right.getMinValue(), min);
-          m.br();
-        }
-        formatValue(m, right.getMaxValue(), max);
-        SafeHtml.set(table, row, 6, m);
-      }
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
-      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 5, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 6, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 6, Gerrit.RESOURCES.css()
-          .projectAdminApprovalCategoryRangeLine());
-
-      setRowItem(row, r);
-    }
-
-    public void showInherited(boolean visible) {
-      for (int r = 0; r < table.getRowCount(); r++) {
-        if (getRowItem(r) != null && getRowItem(r).isInherited()) {
-          table.getRowFormatter().setVisible(r, visible);
-        }
-      }
-    }
-
-    private void formatValue(final SafeHtmlBuilder m, final short v,
-        final ApprovalCategoryValue e) {
-      m.openSpan();
-      m
-          .setStyleName(Gerrit.RESOURCES.css()
-              .projectAdminApprovalCategoryValue());
-      if (v == 0) {
-        m.append(' ');
-      } else if (v > 0) {
-        m.append('+');
-      }
-      m.append(v);
-      m.closeSpan();
-      if (e != null) {
-        m.append(": ");
-        m.append(e.getName());
-      }
-    }
-
-    private boolean getCanDelete() {
-      return canDelete;
-    }
+  private void enable(boolean enabled) {
+    commitMessage.setEnabled(enabled);
+    commit.setEnabled(enabled);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
new file mode 100644
index 0000000..9a74610
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:my='urn:import:com.google.gerrit.client.admin'
+  xmlns:expui='urn:import:com.google.gwtexpui.globalkey.client'
+  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
+  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
+  ui:generateLocales='default,en'
+  >
+<ui:style>
+  @external .gwt-TextArea;
+
+  .commitMessage {
+    margin-top: 2em;
+  }
+  .commitMessage .gwt-TextArea {
+    margin: 5px 5px 5px 5px;
+  }
+</ui:style>
+
+<g:HTMLPanel>
+  <div ui:field='editTools'>
+    <g:Button
+        ui:field='edit'
+        text='Edit'>
+      <ui:attribute name='text'/>
+    </g:Button>
+    <g:Button
+        ui:field='cancel'
+        text='Cancel'>
+      <ui:attribute name='text'/>
+    </g:Button>
+  </div>
+  <my:ProjectAccessEditor ui:field='accessEditor'/>
+  <div ui:field='commitTools'>
+    <div class='{style.commitMessage}'>
+      <ui:msg>Commit Message (optional):</ui:msg><br/>
+      <expui:NpTextArea
+          ui:field='commitMessage'
+          visibleLines='4'
+          characterWidth='60'
+          spellCheck='true'
+          />
+    </div>
+    <g:Button
+        ui:field='commit'
+        text='Save Changes'>
+      <ui:attribute name='text'/>
+    </g:Button>
+    <g:Button
+        ui:field='cancel2'
+        text='Cancel'>
+      <ui:attribute name='text'/>
+    </g:Button>
+  </div>
+  <div style='width: 35em; visibility: hidden;' />
+</g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RefPatternBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RefPatternBox.java
new file mode 100644
index 0000000..a0a3d17
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RefPatternBox.java
@@ -0,0 +1,89 @@
+// 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.client.admin;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.text.shared.Parser;
+import com.google.gwt.text.shared.Renderer;
+import com.google.gwt.user.client.ui.ValueBox;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+
+import java.io.IOException;
+import java.text.ParseException;
+
+public class RefPatternBox extends ValueBox<String> {
+  private static final Renderer<String> RENDERER = new Renderer<String>() {
+    public String render(String ref) {
+      return ref;
+    }
+
+    public void render(String ref, Appendable dst) throws IOException {
+      dst.append(render(ref));
+    }
+  };
+
+  private static final Parser<String> PARSER = new Parser<String>() {
+    public String parse(CharSequence text) throws ParseException {
+      String ref = text.toString();
+
+      if (ref.isEmpty()) {
+        throw new ParseException(Util.C.refErrorEmpty(), 0);
+      }
+
+      if (ref.charAt(0) == '/') {
+        throw new ParseException(Util.C.refErrorBeginSlash(), 0);
+      }
+
+      final boolean re = ref.charAt(0) == '^';
+      if (re && !ref.startsWith("^refs/")) {
+        ref = "^refs/heads/" + ref.substring(1);
+      } else if (!ref.startsWith("refs/")) {
+        ref = "refs/heads/" + ref;
+      }
+
+      for (int i = 0; i < ref.length(); i++) {
+        final char c = ref.charAt(i);
+
+        if (c == '/' && 0 < i && ref.charAt(i - 1) == '/') {
+          throw new ParseException(Util.C.refErrorDoubleSlash(), i);
+        }
+
+        if (c == ' ') {
+          throw new ParseException(Util.C.refErrorNoSpace(), i);
+        }
+
+        if (c < ' ') {
+          throw new ParseException(Util.C.refErrorPrintable(), i);
+        }
+      }
+      return ref;
+    }
+  };
+
+  public RefPatternBox() {
+    super(Document.get().createTextInputElement(), RENDERER, PARSER);
+    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
+    addKeyPressHandler(new KeyPressHandler() {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        if (event.getCharCode() == ' ') {
+          event.preventDefault();
+        }
+      }
+    });
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
index 4167116..c599ee9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
@@ -32,6 +32,8 @@
 
     PROJECT_SVC = GWT.create(ProjectAdminService.class);
     JsonUtil.bind(PROJECT_SVC, "rpc/ProjectAdminService");
+
+    AdminResources.I.css().ensureInjected();
   }
 
   public static String toLongString(final Project.SubmitType type) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.java
new file mode 100644
index 0000000..d8b8c5d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.java
@@ -0,0 +1,206 @@
+// 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.client.admin;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.editor.client.EditorError;
+import com.google.gwt.editor.client.HasEditorErrors;
+import com.google.gwt.editor.client.IsEditor;
+import com.google.gwt.editor.client.LeafValueEditor;
+import com.google.gwt.editor.ui.client.adapters.ValueBoxEditor;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.DoubleClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickHandler;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiChild;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Focusable;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.ValueBoxBase;
+import com.google.gwt.user.client.ui.Widget;
+
+import java.text.ParseException;
+import java.util.List;
+
+public class ValueEditor<T> extends Composite implements HasEditorErrors<T>,
+    IsEditor<ValueBoxEditor<T>>, LeafValueEditor<T>, Focusable {
+  interface Binder extends UiBinder<Widget, ValueEditor<?>> {
+  }
+
+  static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField
+  SimplePanel textPanel;
+  private Label textLabel;
+  private StartEditHandlers startHandlers;
+
+  @UiField
+  Image editIcon;
+
+  @UiField
+  SimplePanel editPanel;
+
+  @UiField
+  DivElement errorLabel;
+
+  private ValueBoxBase<T> editChild;
+  private ValueBoxEditor<T> editProxy;
+
+  public ValueEditor() {
+    startHandlers = new StartEditHandlers();
+    initWidget(uiBinder.createAndBindUi(this));
+    editPanel.setVisible(false);
+    editIcon.addClickHandler(startHandlers);
+  }
+
+  public void edit() {
+    textPanel.removeFromParent();
+    textPanel = null;
+    textLabel = null;
+
+    editIcon.removeFromParent();
+    editIcon = null;
+    startHandlers = null;
+
+    editPanel.setVisible(true);
+  }
+
+  public ValueBoxEditor<T> asEditor() {
+    if (editProxy == null) {
+      editProxy = new EditorProxy();
+    }
+    return editProxy;
+  }
+
+  @Override
+  public T getValue() {
+    return asEditor().getValue();
+  }
+
+  @Override
+  public void setValue(T value) {
+    asEditor().setValue(value);
+  }
+
+  public void setEditTitle(String title) {
+    editIcon.setTitle(title);
+  }
+
+  @UiChild(limit = 1, tagname = "display")
+  public void setDisplay(Label widget) {
+    textLabel = widget;
+    textPanel.add(textLabel);
+
+    textLabel.addClickHandler(startHandlers);
+    textLabel.addDoubleClickHandler(startHandlers);
+  }
+
+  @UiChild(limit = 1, tagname = "editor")
+  public void setEditor(ValueBoxBase<T> widget) {
+    editChild = widget;
+    editPanel.add(editChild);
+    editProxy = null;
+  }
+
+  public void setEnabled(boolean enabled) {
+    editIcon.setVisible(enabled);
+    startHandlers.enabled = enabled;
+  }
+
+  public void showErrors(List<EditorError> errors) {
+    StringBuilder buf = new StringBuilder();
+    for (EditorError error : errors) {
+      if (error.getEditor().equals(editProxy)) {
+        buf.append("\n");
+        if (error.getUserData() instanceof ParseException) {
+          buf.append(((ParseException) error.getUserData()).getMessage());
+        } else {
+          buf.append(error.getMessage());
+        }
+      }
+    }
+
+    if (0 < buf.length()) {
+      errorLabel.setInnerText(buf.substring(1));
+      errorLabel.getStyle().setDisplay(Display.BLOCK);
+    } else {
+      errorLabel.setInnerText("");
+      errorLabel.getStyle().setDisplay(Display.NONE);
+    }
+  }
+
+  @Override
+  public void setAccessKey(char key) {
+    editChild.setAccessKey(key);
+  }
+
+  @Override
+  public void setFocus(boolean focused) {
+    editChild.setFocus(focused);
+    if (focused) {
+      editChild.setCursorPos(editChild.getText().length());
+    }
+  }
+
+  @Override
+  public int getTabIndex() {
+    return editChild.getTabIndex();
+  }
+
+  @Override
+  public void setTabIndex(int index) {
+    editChild.setTabIndex(index);
+  }
+
+  private class StartEditHandlers implements ClickHandler, DoubleClickHandler {
+    boolean enabled;
+
+    @Override
+    public void onClick(ClickEvent event) {
+      if (enabled && event.getNativeButton() == NativeEvent.BUTTON_LEFT) {
+        edit();
+      }
+    }
+
+    @Override
+    public void onDoubleClick(DoubleClickEvent event) {
+      if (enabled && event.getNativeButton() == NativeEvent.BUTTON_LEFT) {
+        edit();
+      }
+    }
+  }
+
+  private class EditorProxy extends ValueBoxEditor<T> {
+    EditorProxy() {
+      super(editChild);
+    }
+
+    @Override
+    public void setValue(T value) {
+      super.setValue(value);
+      if (textLabel == null) {
+        setDisplay(new Label());
+      }
+      textLabel.setText(editChild.getText());
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml
new file mode 100644
index 0000000..9862848
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  >
+<ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
+<ui:style>
+  .panel {
+    position: relative;
+    white-space: nowrap;
+  }
+
+  .textPanel {
+    width: 100%;
+    padding-right: 21px;
+  }
+
+  .editIcon {
+    position: absolute;
+    top: 0;
+    right: 5px;
+  }
+
+  .editPanel {
+    width: 100%;
+  }
+
+  .errorLabel {
+    display: none;
+    color: red;
+    white-space: pre;
+  }
+</ui:style>
+<g:HTMLPanel stylePrimaryName='{style.panel}'>
+  <g:Image
+      ui:field='editIcon'
+      resource='{res.editText}'
+      stylePrimaryName='{style.editIcon}'
+      title='Edit'>
+    <ui:attribute name='title'/>
+  </g:Image>
+  <g:SimplePanel ui:field='textPanel' stylePrimaryName='{style.textPanel}'/>
+
+  <g:SimplePanel ui:field='editPanel' stylePrimaryName='{style.editPanel}'/>
+  <div
+      ui:field='errorLabel'
+      class='{style.errorLabel}'/>
+</g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/admin.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/admin.css
new file mode 100644
index 0000000..eca4823
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/admin.css
@@ -0,0 +1,53 @@
+/* Copyright (C) 2009 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.
+ */
+
+@eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+@eval textColor com.google.gerrit.client.Gerrit.getTheme().textColor;
+@def deletedBackground #a9a9a9;
+
+@sprite .deleteIcon {
+  gwt-image: 'deleteNormal';
+  border: none;
+}
+
+@sprite .deleteIcon:hover {
+  gwt-image: 'deleteHover';
+  border: none;
+}
+
+@sprite .undoIcon {
+  gwt-image: 'undoNormal';
+  border: none;
+}
+
+.deleted {
+  background-color: deletedBackground;
+  color: #ffffff;
+  white-space: nowrap;
+  padding-left: 50px;
+}
+
+.deleted:hover {
+  background-color: selectionColor;
+  color: textColor;
+ }
+
+.deletedBorder {
+  background: 1px solid deletedBackground;
+}
+
+.deleteSectionHover {
+  background-color: selectionColor !important;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteHover.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteHover.png
new file mode 100644
index 0000000..839e8ef
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteHover.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteNormal.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteNormal.png
new file mode 100644
index 0000000..ffddb6f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteNormal.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/editText.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/editText.png
new file mode 100644
index 0000000..188e1c1
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/editText.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/undoNormal.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/undoNormal.png
new file mode 100644
index 0000000..8b0fef9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/undoNormal.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
index 740e08c..f4862ee 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
@@ -27,7 +27,8 @@
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountDiffPreference;
 import com.google.gerrit.reviewdb.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.ApprovalCategory;
+import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ChangeMessage;
 import com.google.gerrit.reviewdb.Patch;
@@ -35,8 +36,6 @@
 import com.google.gerrit.reviewdb.PatchSetInfo;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.UserIdentity;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadScheme;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -54,7 +53,6 @@
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -402,12 +400,8 @@
 
   private void populateActions(final PatchSetDetail detail) {
     final boolean isOpen = changeDetail.getChange().getStatus().isOpen();
-    Set<ApprovalCategory.Id> allowed = changeDetail.getCurrentActions();
-    if (allowed == null) {
-      allowed = Collections.emptySet();
-    }
 
-    if (isOpen && allowed.contains(ApprovalCategory.SUBMIT)) {
+    if (isOpen && changeDetail.canSubmit()) {
       final Button b =
           new Button(Util.M
               .submitPatchSet(detail.getPatchSet().getPatchSetId()));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
index 0ed58b4..f7f8ae1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
@@ -25,8 +25,11 @@
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.PatchSetPublishDetail;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.Change;
@@ -39,11 +42,11 @@
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FormPanel;
+import com.google.gwt.user.client.ui.FormPanel.SubmitEvent;
 import com.google.gwt.user.client.ui.Panel;
 import com.google.gwt.user.client.ui.RadioButton;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwt.user.client.ui.Widget;
-import com.google.gwt.user.client.ui.FormPanel.SubmitEvent;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
 import com.google.gwtjsonrpc.client.VoidResult;
 
@@ -54,7 +57,6 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 public class PublishCommentScreen extends AccountScreen implements
     ClickHandler, CommentEditorContainer {
@@ -218,16 +220,25 @@
   }
 
   private void initApprovals(final PatchSetPublishDetail r, final Panel body) {
-    for (final ApprovalType ct : Gerrit.getConfig().getApprovalTypes()
-        .getApprovalTypes()) {
-      if (r.isAllowed(ct.getCategory().getId())) {
-        initApprovalType(r, body, ct);
+    ApprovalTypes types = Gerrit.getConfig().getApprovalTypes();
+
+    for (ApprovalType type : types.getApprovalTypes()) {
+      String permission = Permission.forLabel(type.getCategory().getLabelName());
+      PermissionRange range = r.getRange(permission);
+      if (range != null && !range.isEmpty()) {
+        initApprovalType(r, body, type, range);
+      }
+    }
+
+    for (PermissionRange range : r.getLabels()) {
+      if (!range.isEmpty() && types.byLabel(range.getLabel()) == null) {
+        // TODO: this is a non-standard label. Offer it without the type.
       }
     }
   }
 
   private void initApprovalType(final PatchSetPublishDetail r,
-      final Panel body, final ApprovalType ct) {
+      final Panel body, final ApprovalType ct, final PermissionRange range) {
     body.add(new SmallHeading(ct.getCategory().getName() + ":"));
 
     final VerticalPanel vp = new VerticalPanel();
@@ -236,11 +247,10 @@
         new ArrayList<ApprovalCategoryValue>(ct.getValues());
     Collections.reverse(lst);
     final ApprovalCategory.Id catId = ct.getCategory().getId();
-    final Set<ApprovalCategoryValue.Id> allowed = r.getAllowed(catId);
     final PatchSetApproval prior = r.getChangeApproval(catId);
 
     for (final ApprovalCategoryValue buttonValue : lst) {
-      if (!allowed.contains(buttonValue.getId())) {
+      if (!range.contains(buttonValue.getValue())) {
         continue;
       }
 
@@ -306,7 +316,7 @@
       }
     }
 
-    submit.setVisible(r.isSubmitAllowed());
+    submit.setVisible(r.canSubmit());
   }
 
   private void onSend(final boolean submit) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
index 6452e08..d5a5367 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
@@ -175,7 +175,7 @@
 
   protected abstract void onInsertComment(PatchLine pl);
 
-  public abstract void display(CommentDetail comments);
+  public abstract void display(CommentDetail comments, boolean expandComments);
 
   @Override
   protected MyFlexTable createFlexTable() {
@@ -529,7 +529,7 @@
   }
 
   protected void bindComment(final int row, final int col,
-      final PatchLineComment line, final boolean isLast) {
+      final PatchLineComment line, final boolean isLast, boolean expandComment) {
     if (line.getStatus() == PatchLineComment.Status.DRAFT) {
       final CommentEditorPanel plc = new CommentEditorPanel(line);
       plc.addFocusHandler(this);
@@ -541,6 +541,7 @@
       final AccountInfo author = accountCache.get(line.getAuthor());
       final PublishedCommentPanel panel =
           new PublishedCommentPanel(author, line);
+      panel.setOpen(expandComment);
       panel.addFocusHandler(this);
       panel.addBlurHandler(this);
       table.setWidget(row, col, panel);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
index 736d1f0..df523b9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
@@ -438,7 +438,7 @@
 
     if (hasDifferences) {
       contentTable.display(patchKey, idSideA, idSideB, script);
-      contentTable.display(script.getCommentDetail());
+      contentTable.display(script.getCommentDetail(), script.isExpandAllComments());
       contentTable.finishDisplay();
     }
     showPatch(hasDifferences);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
index 404eeeb..bce7f55 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
@@ -87,6 +87,9 @@
   @UiField
   CheckBox skipUncommented;
 
+  @UiField
+  CheckBox expandAllComments;
+
 
   @UiField
   Button update;
@@ -209,6 +212,7 @@
     showTabs.setValue(dp.isShowTabs());
     skipDeleted.setValue(dp.isSkipDeleted());
     skipUncommented.setValue(dp.isSkipUncommented());
+    expandAllComments.setValue(dp.isExpandAllComments());
   }
 
   @UiHandler("update")
@@ -233,6 +237,7 @@
     dp.setShowTabs(showTabs.getValue());
     dp.setSkipDeleted(skipDeleted.getValue());
     dp.setSkipUncommented(skipUncommented.getValue());
+    dp.setExpandAllComments(expandAllComments.getValue());
 
     listenablePrefs.set(dp);
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
index f0f1b4d..8bda032 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
@@ -115,18 +115,27 @@
       </g:CheckBox>
     </td>
 
+    <td>
+      <g:CheckBox
+          ui:field='expandAllComments'
+          text='Expand All Comments'
+          tabIndex='9'>
+        <ui:attribute name='text'/>
+      </g:CheckBox>
+    </td>
+
     <td rowspan='2'>
       <g:CheckBox
           ui:field='skipUncommented'
           text='Skip Uncommented Files'
-          tabIndex='9'>
+          tabIndex='10'>
         <ui:attribute name='text'/>
       </g:CheckBox>
       <br/>
       <g:CheckBox
           ui:field='skipDeleted'
           text='Skip Deleted Files'
-          tabIndex='10'>
+          tabIndex='11'>
         <ui:attribute name='text'/>
       </g:CheckBox>
     </td>
@@ -136,7 +145,7 @@
           ui:field='update'
           text='Update'
           styleName='{style.updateButton}'
-          tabIndex='11'>
+          tabIndex='12'>
         <ui:attribute name='text'/>
       </g:Button>
     </td>
@@ -145,7 +154,7 @@
       <g:CheckBox
           ui:field='reviewed'
           text='Reviewed'
-          tabIndex='12'>
+          tabIndex='13'>
         <ui:attribute name='text'/>
       </g:CheckBox>
     </td>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
index 2939e71..1c27388 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
@@ -186,7 +186,7 @@
   }
 
   @Override
-  public void display(final CommentDetail cd) {
+  public void display(final CommentDetail cd, boolean expandComments) {
     if (cd.isEmpty()) {
       return;
     }
@@ -205,13 +205,13 @@
           final PatchLineComment ac = ai.next();
           final PatchLineComment bc = bi.next();
           insertRow(row);
-          bindComment(row, COL_A, ac, !ai.hasNext());
-          bindComment(row, COL_B, bc, !bi.hasNext());
+          bindComment(row, COL_A, ac, !ai.hasNext(), expandComments);
+          bindComment(row, COL_B, bc, !bi.hasNext(), expandComments);
           row++;
         }
 
-        row = finish(ai, row, COL_A);
-        row = finish(bi, row, COL_B);
+        row = finish(ai, row, COL_A, expandComments);
+        row = finish(bi, row, COL_B, expandComments);
       } else {
         row++;
       }
@@ -228,11 +228,11 @@
     fmt.addStyleName(row, COL_B, Gerrit.RESOURCES.css().diffText());
   }
 
-  private int finish(final Iterator<PatchLineComment> i, int row, final int col) {
+  private int finish(final Iterator<PatchLineComment> i, int row, final int col, boolean expandComment) {
     while (i.hasNext()) {
       final PatchLineComment c = i.next();
       insertRow(row);
-      bindComment(row, col, c, !i.hasNext());
+      bindComment(row, col, c, !i.hasNext(), expandComment);
       row++;
     }
     return row;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
index 508e508..21a5211 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
@@ -205,7 +205,7 @@
   }
 
   @Override
-  public void display(final CommentDetail cd) {
+  public void display(final CommentDetail cd, boolean expandComments) {
     if (cd.isEmpty()) {
       return;
     }
@@ -224,13 +224,13 @@
           all.addAll(fora);
           all.addAll(forb);
           Collections.sort(all, BY_DATE);
-          row = insert(all, row);
+          row = insert(all, row, expandComments);
 
         } else if (!fora.isEmpty()) {
-          row = insert(fora, row);
+          row = insert(fora, row, expandComments);
 
         } else if (!forb.isEmpty()) {
-          row = insert(forb, row);
+          row = insert(forb, row, expandComments);
         }
       } else {
         row++;
@@ -248,11 +248,11 @@
     fmt.addStyleName(row, PC, Gerrit.RESOURCES.css().diffText());
   }
 
-  private int insert(final List<PatchLineComment> in, int row) {
+  private int insert(final List<PatchLineComment> in, int row, boolean expandComment) {
     for (Iterator<PatchLineComment> ci = in.iterator(); ci.hasNext();) {
       final PatchLineComment c = ci.next();
       insertRow(row);
-      bindComment(row, PC, c, !ci.hasNext());
+      bindComment(row, PC, c, !ci.hasNext(), expandComment);
       row++;
     }
     return row;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
index 0a9cedf..5d8ee8b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
@@ -16,26 +16,34 @@
 
 import com.google.gerrit.client.RpcStatus;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.reviewdb.AccountGroupName;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gwt.user.client.ui.SuggestOracle;
 import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /** Suggestion Oracle for AccountGroup entities. */
 public class AccountGroupSuggestOracle extends HighlightSuggestOracle {
+  private Map<String, AccountGroup.UUID> priorResults =
+      new HashMap<String, AccountGroup.UUID>();
+
   @Override
   public void onRequestSuggestions(final Request req, final Callback callback) {
     RpcStatus.hide(new Runnable() {
       public void run() {
         SuggestUtil.SVC.suggestAccountGroup(req.getQuery(), req.getLimit(),
-            new GerritCallback<List<AccountGroupName>>() {
-              public void onSuccess(final List<AccountGroupName> result) {
+            new GerritCallback<List<GroupReference>>() {
+              public void onSuccess(final List<GroupReference> result) {
+                priorResults.clear();
                 final ArrayList<AccountGroupSuggestion> r =
                     new ArrayList<AccountGroupSuggestion>(result.size());
-                for (final AccountGroupName p : result) {
+                for (final GroupReference p : result) {
                   r.add(new AccountGroupSuggestion(p));
+                  priorResults.put(p.getName(), p.getUUID());
                 }
                 callback.onSuggestionsReady(req, new Response(r));
               }
@@ -46,9 +54,9 @@
 
   private static class AccountGroupSuggestion implements
       SuggestOracle.Suggestion {
-    private final AccountGroupName info;
+    private final GroupReference info;
 
-    AccountGroupSuggestion(final AccountGroupName k) {
+    AccountGroupSuggestion(final GroupReference k) {
       info = k;
     }
 
@@ -60,4 +68,9 @@
       return info.getName();
     }
   }
+
+  /** @return the group UUID, or null if it cannot be found. */
+  public AccountGroup.UUID getUUID(String name) {
+    return priorResults.get(name);
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java
index 6c18db1..1261b42 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java
@@ -25,6 +25,10 @@
 public class Hyperlink extends com.google.gwt.user.client.ui.Hyperlink {
   static final HyperlinkImpl impl = GWT.create(HyperlinkImpl.class);
 
+  /** Initialize a default hyperlink with no target and no text. */
+  public Hyperlink() {
+  }
+
   /**
    * Creates a hyperlink with its text and target history token specified.
    *
diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml
index 2a9fa7d..5154515 100644
--- a/gerrit-httpd/pom.xml
+++ b/gerrit-httpd/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-httpd</artifactId>
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
index 379fee6..adc48f8 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.httpd.rpc;
 
 import com.google.gerrit.common.data.AccountInfo;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.SuggestService;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroup.Id;
 import com.google.gerrit.reviewdb.AccountGroupName;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.ReviewDb;
@@ -30,8 +30,10 @@
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
@@ -52,23 +54,28 @@
   private static final String MAX_SUFFIX = "\u9fa5";
 
   private final AuthConfig authConfig;
+  private final ProjectControl.Factory projectControlFactory;
   private final ProjectCache projectCache;
   private final AccountCache accountCache;
   private final GroupControl.Factory groupControlFactory;
   private final IdentifiedUser.GenericFactory userFactory;
   private final Provider<CurrentUser> currentUser;
   private final SuggestAccountsEnum suggestAccounts;
+  private final GroupCache groupCache;
 
   @Inject
   SuggestServiceImpl(final Provider<ReviewDb> schema,
       final AuthConfig authConfig,
+      final ProjectControl.Factory projectControlFactory,
       final ProjectCache projectCache, final AccountCache accountCache,
       final GroupControl.Factory groupControlFactory,
       final IdentifiedUser.GenericFactory userFactory,
       final Provider<CurrentUser> currentUser,
-      @GerritServerConfig final Config cfg) {
+      @GerritServerConfig final Config cfg,
+      final GroupCache groupCache) {
     super(schema, currentUser);
     this.authConfig = authConfig;
+    this.projectControlFactory = projectControlFactory;
     this.projectCache = projectCache;
     this.accountCache = accountCache;
     this.groupControlFactory = groupControlFactory;
@@ -76,28 +83,29 @@
     this.currentUser = currentUser;
     this.suggestAccounts =
         cfg.getEnum("suggest", null, "accounts", SuggestAccountsEnum.ALL);
+    this.groupCache = groupCache;
   }
 
   public void suggestProjectNameKey(final String query, final int limit,
       final AsyncCallback<List<Project.NameKey>> callback) {
-    run(callback, new Action<List<Project.NameKey>>() {
-      public List<Project.NameKey> run(final ReviewDb db) throws OrmException {
-        final String a = query;
-        final String b = a + MAX_SUFFIX;
-        final int max = 10;
-        final int n = limit <= 0 ? max : Math.min(limit, max);
+    final int max = 10;
+    final int n = limit <= 0 ? max : Math.min(limit, max);
 
-        final CurrentUser user = currentUser.get();
-        final List<Project.NameKey> r = new ArrayList<Project.NameKey>();
-        for (final Project p : db.projects().suggestByName(a, b, n)) {
-          final ProjectState e = projectCache.get(p.getNameKey());
-          if (e != null && e.controlFor(user).isVisible()) {
-            r.add(p.getNameKey());
-          }
-        }
-        return r;
+    final List<Project.NameKey> r = new ArrayList<Project.NameKey>(n);
+    for (final Project.NameKey nameKey : projectCache.byName(query)) {
+      final ProjectControl ctl;
+      try {
+        ctl = projectControlFactory.validateFor(nameKey);
+      } catch (NoSuchProjectException e) {
+        continue;
       }
-    });
+
+      r.add(ctl.getProject().getNameKey());
+      if (r.size() == n) {
+        break;
+      }
+    }
+    callback.onSuccess(r);
   }
 
   public void suggestAccount(final String query, final Boolean active,
@@ -154,10 +162,11 @@
         map.put(account.getId(), info);
         break;
       case SAME_GROUP: {
-        Set<AccountGroup.Id> usersGroups = groupsOf(account);
-        usersGroups.removeAll(authConfig.getRegisteredGroups());
+        Set<AccountGroup.UUID> usersGroups = groupsOf(account);
+        usersGroups.remove(AccountGroup.ANONYMOUS_USERS);
+        usersGroups.remove(AccountGroup.REGISTERED_USERS);
         usersGroups.remove(authConfig.getBatchUsersGroup());
-        for (AccountGroup.Id myGroup : currentUser.get().getEffectiveGroups()) {
+        for (AccountGroup.UUID myGroup : currentUser.get().getEffectiveGroups()) {
           if (usersGroups.contains(myGroup)) {
             map.put(account.getId(), info);
             break;
@@ -166,10 +175,11 @@
         break;
       }
       case VISIBLE_GROUP: {
-        Set<AccountGroup.Id> usersGroups = groupsOf(account);
-        usersGroups.removeAll(authConfig.getRegisteredGroups());
+        Set<AccountGroup.UUID> usersGroups = groupsOf(account);
+        usersGroups.remove(AccountGroup.ANONYMOUS_USERS);
+        usersGroups.remove(AccountGroup.REGISTERED_USERS);
         usersGroups.remove(authConfig.getBatchUsersGroup());
-        for (AccountGroup.Id usersGroup : usersGroups) {
+        for (AccountGroup.UUID usersGroup : usersGroups) {
           try {
             if (groupControlFactory.controlFor(usersGroup).isVisible()) {
               map.put(account.getId(), info);
@@ -188,33 +198,36 @@
     }
   }
 
-  private Set<Id> groupsOf(Account account) {
+  private Set<AccountGroup.UUID> groupsOf(Account account) {
     IdentifiedUser user = userFactory.create(account.getId());
-    return new HashSet<AccountGroup.Id>(user.getEffectiveGroups());
+    return new HashSet<AccountGroup.UUID>(user.getEffectiveGroups());
   }
 
   public void suggestAccountGroup(final String query, final int limit,
-      final AsyncCallback<List<AccountGroupName>> callback) {
-    run(callback, new Action<List<AccountGroupName>>() {
-      public List<AccountGroupName> run(final ReviewDb db) throws OrmException {
+      final AsyncCallback<List<GroupReference>> callback) {
+    run(callback, new Action<List<GroupReference>>() {
+      public List<GroupReference> run(final ReviewDb db) throws OrmException {
         final String a = query;
         final String b = a + MAX_SUFFIX;
         final int max = 10;
         final int n = limit <= 0 ? max : Math.min(limit, max);
-        Set<AccountGroup.Id> memberOf = currentUser.get().getEffectiveGroups();
-        List<AccountGroupName> names = new ArrayList<AccountGroupName>(n);
+        Set<AccountGroup.UUID> memberOf = currentUser.get().getEffectiveGroups();
+        List<GroupReference> r = new ArrayList<GroupReference>(n);
         for (AccountGroupName group : db.accountGroupNames()
               .suggestByName(a, b, n)) {
           try {
             if (memberOf.contains(group.getId())
                 || groupControlFactory.controlFor(group.getId()).isVisible()) {
-              names.add(group);
+              AccountGroup g = groupCache.get(group.getId());
+              if (g != null && g.getGroupUUID() != null) {
+                r.add(GroupReference.forGroup(g));
+              }
             }
           } catch (NoSuchGroupException e) {
             continue;
           }
         }
-        return names;
+        return r;
       }
     });
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
index f638d48..4f62330 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.reviewdb.ContributorAgreement;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.inject.Inject;
 
 import java.util.ArrayList;
@@ -36,13 +37,16 @@
   }
 
   private final ReviewDb db;
+  private final GroupCache groupCache;
   private final IdentifiedUser user;
 
   private AgreementInfo info;
 
   @Inject
-  AgreementInfoFactory(final ReviewDb db, final IdentifiedUser user) {
+  AgreementInfoFactory(final ReviewDb db, final GroupCache groupCache,
+      final IdentifiedUser user) {
     this.db = db;
+    this.groupCache = groupCache;
     this.user = user;
   }
 
@@ -55,9 +59,14 @@
 
     final List<AccountGroupAgreement> groupAccepted =
         new ArrayList<AccountGroupAgreement>();
-    for (final AccountGroup.Id groupId : user.getEffectiveGroups()) {
+    for (final AccountGroup.UUID groupUUID : user.getEffectiveGroups()) {
+      AccountGroup group = groupCache.get(groupUUID);
+      if (group == null) {
+        continue;
+      }
+
       final List<AccountGroupAgreement> temp =
-          db.accountGroupAgreements().byGroup(groupId).toList();
+          db.accountGroupAgreements().byGroup(group.getId()).toList();
 
       Collections.reverse(temp);
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
index a564907..578e866 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
@@ -46,6 +46,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
@@ -118,8 +119,14 @@
     createGroupFactory.create(newName).to(callback);
   }
 
-  public void groupDetail(final AccountGroup.Id groupId,
-      final AsyncCallback<GroupDetail> callback) {
+  public void groupDetail(AccountGroup.Id groupId, AccountGroup.UUID groupUUID,
+      AsyncCallback<GroupDetail> callback) {
+    if (groupId == null && groupUUID != null) {
+      AccountGroup g = groupCache.get(groupUUID);
+      if (g != null) {
+        groupId = g.getId();
+      }
+    }
     groupDetailFactory.create(groupId).to(callback);
   }
 
@@ -281,7 +288,7 @@
               Collections.singleton(new AccountGroupIncludeAudit(m,
                   getAccountId())));
           db.accountGroupIncludes().insert(Collections.singleton(m));
-          groupIncludeCache.evictInclude(a.getId());
+          groupIncludeCache.evictInclude(a.getGroupUUID());
         }
 
         return groupDetailFactory.create(groupId).call();
@@ -361,6 +368,7 @@
         }
 
         final Account.Id me = getAccountId();
+        final Set<AccountGroup.Id> groupsToEvict = new HashSet<AccountGroup.Id>();
         for (final AccountGroupInclude.Key k : keys) {
           final AccountGroupInclude m =
               db.accountGroupIncludes().get(k);
@@ -385,9 +393,12 @@
                   Collections.singleton(audit));
             }
             db.accountGroupIncludes().delete(Collections.singleton(m));
-            groupIncludeCache.evictInclude(m.getIncludeId());
+            groupsToEvict.add(k.getIncludeId());
           }
         }
+        for (AccountGroup group : db.accountGroups().get(groupsToEvict)) {
+          groupIncludeCache.evictInclude(group.getGroupUUID());
+        }
         return VoidResult.INSTANCE;
       }
     });
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java
index b3e993e..088fae2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java
@@ -42,10 +42,10 @@
 
   @Override
   public List<AccountGroup> call() throws Exception {
-    final Set<AccountGroup.Id> effective = user.getEffectiveGroups();
+    final Set<AccountGroup.UUID> effective = user.getEffectiveGroups();
     final int cnt = effective.size();
     final List<AccountGroup> groupList = new ArrayList<AccountGroup>(cnt);
-    for (final AccountGroup.Id groupId : effective) {
+    for (final AccountGroup.UUID groupId : effective) {
       groupList.add(groupCache.get(groupId));
     }
     Collections.sort(groupList, new Comparator<AccountGroup>() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
index d62f0c0..c8bf519 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
@@ -21,14 +21,19 @@
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.AccountGroupName;
 import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.git.RenameGroupOp;
 import com.google.gwtorm.client.OrmDuplicateKeyException;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 import java.util.Collections;
+import java.util.Date;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
 
 class RenameGroup extends Handler<GroupDetail> {
   interface Factory {
@@ -39,6 +44,8 @@
   private final GroupCache groupCache;
   private final GroupControl.Factory groupControlFactory;
   private final GroupDetailFactory.Factory groupDetailFactory;
+  private final RenameGroupOp.Factory renameGroupOpFactory;
+  private final IdentifiedUser currentUser;
 
   private final AccountGroup.Id groupId;
   private final String newName;
@@ -47,11 +54,15 @@
   RenameGroup(final ReviewDb db, final GroupCache groupCache,
       final GroupControl.Factory groupControlFactory,
       final GroupDetailFactory.Factory groupDetailFactory,
+      final RenameGroupOp.Factory renameGroupOpFactory,
+      final IdentifiedUser currentUser,
       @Assisted final AccountGroup.Id groupId, @Assisted final String newName) {
     this.db = db;
     this.groupCache = groupCache;
     this.groupControlFactory = groupControlFactory;
     this.groupDetailFactory = groupDetailFactory;
+    this.renameGroupOpFactory = renameGroupOpFactory;
+    this.currentUser = currentUser;
     this.groupId = groupId;
     this.newName = newName;
   }
@@ -94,6 +105,10 @@
 
     groupCache.evict(group);
     groupCache.evictAfterRename(old);
+    renameGroupOpFactory.create( //
+        currentUser.newCommitterIdent(new Date(), TimeZone.getDefault()), //
+        group.getGroupUUID(), //
+        old.get(), newName).start(0, TimeUnit.MILLISECONDS);
 
     return groupDetailFactory.create(groupId).call();
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java
index d2f59f5..8eb1bbe 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
@@ -71,7 +72,8 @@
 
   @Override
   public ChangeDetail call() throws NoSuchChangeException, OrmException,
-      EmailException, NoSuchEntityException, PatchSetInfoNotAvailableException {
+      EmailException, NoSuchEntityException, InvalidChangeOperationException,
+      PatchSetInfoNotAvailableException {
 
     final Change.Id changeId = patchSetId.getParentKey();
     final ChangeControl control = changeControlFactory.validateFor(changeId);
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 4016641..075d24e 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
@@ -33,6 +33,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.CanSubmitResult;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.workflow.CategoryFunction;
@@ -94,6 +95,7 @@
     if (patch == null) {
       throw new NoSuchEntityException();
     }
+    final CanSubmitResult canSubmitResult = control.canSubmit(patch.getId());
 
     aic.want(change.getOwner());
 
@@ -103,6 +105,7 @@
 
     detail.setCanAbandon(change.getStatus().isOpen() && control.canAbandon());
     detail.setCanRestore(change.getStatus() == Change.Status.ABANDONED && control.canRestore());
+    detail.setCanSubmit(canSubmitResult == CanSubmitResult.OK);
     detail.setStarred(control.getCurrentUser().getStarredChanges().contains(
         changeId));
 
@@ -141,23 +144,13 @@
       final Set<ApprovalCategory.Id> missingApprovals =
           new HashSet<ApprovalCategory.Id>();
 
-      final Set<ApprovalCategory.Id> currentActions =
-          new HashSet<ApprovalCategory.Id>();
-
       for (final ApprovalType at : approvalTypes.getApprovalTypes()) {
         CategoryFunction.forCategory(at.getCategory()).run(at, fs);
         if (!fs.isValid(at)) {
           missingApprovals.add(at.getCategory().getId());
         }
       }
-      for (final ApprovalType at : approvalTypes.getActionTypes()) {
-        if (CategoryFunction.forCategory(at.getCategory()).isValid(
-            control.getCurrentUser(), at, fs)) {
-          currentActions.add(at.getCategory().getId());
-        }
-      }
       detail.setMissingApprovals(missingApprovals);
-      detail.setCurrentActions(currentActions);
     }
 
     final boolean canRemoveReviewers = detail.getChange().getStatus().isOpen() //
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
index 779475e..58daa09 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
@@ -15,19 +15,14 @@
 package com.google.gerrit.httpd.rpc.changedetail;
 
 import com.google.gerrit.common.data.AccountInfoCache;
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.PatchSetPublishDetail;
+import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchLineComment;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gerrit.reviewdb.PatchSetInfo;
-import com.google.gerrit.reviewdb.RefRight;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
@@ -36,27 +31,20 @@
 import com.google.gerrit.server.project.CanSubmitResult;
 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.project.RefControl;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import java.util.HashMap;
-import java.util.HashSet;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 final class PatchSetPublishDetailFactory extends Handler<PatchSetPublishDetail> {
   interface Factory {
     PatchSetPublishDetailFactory create(PatchSet.Id patchSetId);
   }
 
-  private final ProjectCache projectCache;
   private final PatchSetInfoFactory infoFactory;
-  private final ApprovalTypes approvalTypes;
   private final ReviewDb db;
   private final ChangeControl.Factory changeControlFactory;
   private final AccountInfoCacheFactory aic;
@@ -68,19 +56,14 @@
   private PatchSetInfo patchSetInfo;
   private Change change;
   private List<PatchLineComment> drafts;
-  private Map<ApprovalCategory.Id, Set<ApprovalCategoryValue.Id>> allowed;
-  private Map<ApprovalCategory.Id, PatchSetApproval> given;
 
   @Inject
   PatchSetPublishDetailFactory(final PatchSetInfoFactory infoFactory,
-      final ProjectCache projectCache, final ApprovalTypes approvalTypes,
       final ReviewDb db,
       final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
       final ChangeControl.Factory changeControlFactory,
       final IdentifiedUser user, @Assisted final PatchSet.Id patchSetId) {
-    this.projectCache = projectCache;
     this.infoFactory = infoFactory;
-    this.approvalTypes = approvalTypes;
     this.db = db;
     this.changeControlFactory = changeControlFactory;
     this.aic = accountInfoCacheFactory.create();
@@ -98,15 +81,17 @@
     patchSetInfo = infoFactory.get(patchSetId);
     drafts = db.patchComments().draft(patchSetId, user.getAccountId()).toList();
 
-    allowed = new HashMap<ApprovalCategory.Id, Set<ApprovalCategoryValue.Id>>();
-    given = new HashMap<ApprovalCategory.Id, PatchSetApproval>();
+    List<PermissionRange> allowed = Collections.emptyList();
+    List<PatchSetApproval> given = Collections.emptyList();
+
     if (change.getStatus().isOpen()
         && patchSetId.equals(change.currentPatchSetId())) {
-      computeAllowed();
-      for (final PatchSetApproval a : db.patchSetApprovals().byPatchSetUser(
-          patchSetId, user.getAccountId())) {
-        given.put(a.getCategoryId(), a);
-      }
+      allowed = new ArrayList<PermissionRange>(control.getLabelRanges());
+      Collections.sort(allowed);
+
+      given = db.patchSetApprovals() //
+          .byPatchSetUser(patchSetId, user.getAccountId()) //
+          .toList();
     }
 
     aic.want(change.getOwner());
@@ -117,46 +102,12 @@
     detail.setPatchSetInfo(patchSetInfo);
     detail.setChange(change);
     detail.setDrafts(drafts);
-    detail.setAllowed(allowed);
+    detail.setLabels(allowed);
     detail.setGiven(given);
 
     final CanSubmitResult canSubmitResult = control.canSubmit(patchSetId);
-    detail.setSubmitAllowed(canSubmitResult == CanSubmitResult.OK);
+    detail.setCanSubmit(canSubmitResult == CanSubmitResult.OK);
 
     return detail;
   }
-
-  private void computeAllowed() {
-    final Set<AccountGroup.Id> am = user.getEffectiveGroups();
-    final ProjectState pe = projectCache.get(change.getProject());
-    for (ApprovalCategory.Id category : approvalTypes.getApprovalCategories()) {
-      RefControl rc = pe.controlFor(user).controlForRef(change.getDest());
-      List<RefRight> categoryRights = rc.getApplicableRights(category);
-      computeAllowed(am, categoryRights, category);
-    }
-  }
-
-  private void computeAllowed(final Set<AccountGroup.Id> am,
-      final List<RefRight> list, ApprovalCategory.Id category) {
-
-    Set<ApprovalCategoryValue.Id> s = allowed.get(category);
-    if (s == null) {
-      s = new HashSet<ApprovalCategoryValue.Id>();
-      allowed.put(category, s);
-    }
-
-    for (final RefRight r : list) {
-      if (!am.contains(r.getAccountGroupId())) {
-        continue;
-      }
-      final ApprovalType at =
-          approvalTypes.getApprovalType(r.getApprovalCategoryId());
-      for (short m = r.getMinValue(); m <= r.getMaxValue(); m++) {
-        final ApprovalCategoryValue v = at.getValue(m);
-        if (v != null) {
-          s.add(v.getId());
-        }
-      }
-    }
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChange.java
index f951439..fa8785a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChange.java
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
@@ -69,7 +70,8 @@
 
   @Override
   public ChangeDetail call() throws NoSuchChangeException, OrmException,
-      EmailException, NoSuchEntityException, PatchSetInfoNotAvailableException {
+      EmailException, NoSuchEntityException, InvalidChangeOperationException,
+      PatchSetInfoNotAvailableException {
 
     final Change.Id changeId = patchSetId.getParentKey();
     final ChangeControl control = changeControlFactory.validateFor(changeId);
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 da52596..c81c2e9 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
@@ -187,11 +187,13 @@
             for (final PatchSetApproval ca : db.patchSetApprovals()
                 .byPatchSetUser(ps_id, aid)) {
               final ApprovalCategory.Id category = ca.getCategoryId();
-              if (change.getStatus().isOpen()) {
-                fs.normalize(approvalTypes.getApprovalType(category), ca);
+              if (ApprovalCategory.SUBMIT.equals(category)) {
+                continue;
               }
-              if (ca.getValue() == 0
-                  || ApprovalCategory.SUBMIT.equals(category)) {
+              if (change.getStatus().isOpen()) {
+                fs.normalize(approvalTypes.byId(category), ca);
+              }
+              if (ca.getValue() == 0) {
                 continue;
               }
               psas.put(category, ca);
@@ -231,11 +233,13 @@
 
             for (PatchSetApproval ca : db.patchSetApprovals().byPatchSet(ps_id)) {
               final ApprovalCategory.Id category = ca.getCategoryId();
-              if (change.getStatus().isOpen()) {
-                fs.normalize(approvalTypes.getApprovalType(category), ca);
+              if (ApprovalCategory.SUBMIT.equals(category)) {
+                continue;
               }
-              if (ca.getValue() == 0
-                  || ApprovalCategory.SUBMIT.equals(category)) {
+              if (change.getStatus().isOpen()) {
+                fs.normalize(approvalTypes.byId(category), ca);
+              }
+              if (ca.getValue() == 0) {
                 continue;
               }
               boolean keep = true;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
deleted file mode 100644
index 358b542..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
+++ /dev/null
@@ -1,207 +0,0 @@
-// Copyright (C) 2010 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.httpd.rpc.project;
-
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.common.errors.InvalidNameException;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.NoSuchRefException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.RefControl;
-import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.Repository;
-
-import java.util.Collections;
-
-import javax.annotation.Nullable;
-
-class AddRefRight extends Handler<ProjectDetail> {
-  interface Factory {
-    AddRefRight create(@Assisted Project.NameKey projectName,
-        @Assisted ApprovalCategory.Id categoryId,
-        @Assisted("groupName") String groupName,
-        @Nullable @Assisted("refPattern") String refPattern,
-        @Assisted("min") short min, @Assisted("max") short max);
-  }
-
-  private final ProjectDetailFactory.Factory projectDetailFactory;
-  private final ProjectControl.Factory projectControlFactory;
-  private final ProjectCache projectCache;
-  private final GroupCache groupCache;
-  private final ReviewDb db;
-  private final ApprovalTypes approvalTypes;
-
-  private final Project.NameKey projectName;
-  private final ApprovalCategory.Id categoryId;
-  private final AccountGroup.NameKey groupName;
-  private final String refPattern;
-  private final short min;
-  private final short max;
-
-  @Inject
-  AddRefRight(final ProjectDetailFactory.Factory projectDetailFactory,
-      final ProjectControl.Factory projectControlFactory,
-      final ProjectCache projectCache, final GroupCache groupCache,
-      final ReviewDb db, final ApprovalTypes approvalTypes,
-
-      @Assisted final Project.NameKey projectName,
-      @Assisted final ApprovalCategory.Id categoryId,
-      @Assisted("groupName") final String groupName,
-      @Nullable @Assisted("refPattern") final String refPattern,
-      @Assisted("min") final short min, @Assisted("max") final short max) {
-    this.projectDetailFactory = projectDetailFactory;
-    this.projectControlFactory = projectControlFactory;
-    this.projectCache = projectCache;
-    this.groupCache = groupCache;
-    this.approvalTypes = approvalTypes;
-    this.db = db;
-
-    this.projectName = projectName;
-    this.categoryId = categoryId;
-    this.groupName = new AccountGroup.NameKey(groupName);
-    this.refPattern = refPattern != null ? refPattern.trim() : null;
-
-    if (min <= max) {
-      this.min = min;
-      this.max = max;
-    } else {
-      this.min = max;
-      this.max = min;
-    }
-  }
-
-  @Override
-  public ProjectDetail call() throws NoSuchProjectException, OrmException,
-      NoSuchGroupException, InvalidNameException, NoSuchRefException {
-    final ProjectControl projectControl =
-        projectControlFactory.controlFor(projectName);
-
-    final ApprovalType at = approvalTypes.getApprovalType(categoryId);
-    if (at == null || at.getValue(min) == null || at.getValue(max) == null) {
-      throw new IllegalArgumentException("Invalid category " + categoryId
-          + " or range " + min + ".." + max);
-    }
-
-    String refPattern = this.refPattern;
-    if (refPattern == null || refPattern.isEmpty()) {
-      if (categoryId.equals(ApprovalCategory.SUBMIT)
-          || categoryId.equals(ApprovalCategory.PUSH_HEAD)) {
-        // Explicitly related to a branch head.
-        refPattern = Constants.R_HEADS + "*";
-
-      } else if (!at.getCategory().isAction()) {
-        // Non actions are approval votes on a change, assume these apply
-        // to branch heads only.
-        refPattern = Constants.R_HEADS + "*";
-
-      } else if (categoryId.equals(ApprovalCategory.PUSH_TAG)) {
-        // Explicitly related to the tag namespace.
-        refPattern = Constants.R_TAGS + "*";
-
-      } else if (categoryId.equals(ApprovalCategory.READ)
-          || categoryId.equals(ApprovalCategory.OWN)) {
-        // Currently these are project-wide rights, so apply that way.
-        refPattern = RefRight.ALL;
-
-      } else {
-        // Assume project wide for the default.
-        refPattern = RefRight.ALL;
-      }
-    }
-
-    boolean exclusive = refPattern.startsWith("-");
-    if (exclusive) {
-      refPattern = refPattern.substring(1);
-    }
-
-    while (refPattern.startsWith("/")) {
-      refPattern = refPattern.substring(1);
-    }
-
-    if (refPattern.startsWith(RefRight.REGEX_PREFIX)) {
-      String example = RefControl.shortestExample(refPattern);
-
-      if (!example.startsWith(Constants.R_REFS)) {
-        refPattern = RefRight.REGEX_PREFIX + Constants.R_HEADS
-                + refPattern.substring(RefRight.REGEX_PREFIX.length());
-        example = RefControl.shortestExample(refPattern);
-      }
-
-      if (!Repository.isValidRefName(example)) {
-        throw new InvalidNameException();
-      }
-
-    } else {
-      if (!refPattern.startsWith(Constants.R_REFS)) {
-        refPattern = Constants.R_HEADS + refPattern;
-      }
-
-      if (refPattern.endsWith("/*")) {
-        final String prefix = refPattern.substring(0, refPattern.length() - 2);
-        if (!"refs".equals(prefix) && !Repository.isValidRefName(prefix)) {
-          throw new InvalidNameException();
-        }
-      } else {
-        if (!Repository.isValidRefName(refPattern)) {
-          throw new InvalidNameException();
-        }
-      }
-    }
-
-    if (!projectControl.controlForRef(refPattern).isOwner()) {
-      throw new NoSuchRefException(refPattern);
-    }
-
-    if (exclusive) {
-      refPattern = "-" + refPattern;
-    }
-
-    final AccountGroup group = groupCache.get(groupName);
-    if (group == null) {
-      throw new NoSuchGroupException(groupName);
-    }
-    final RefRight.Key key =
-        new RefRight.Key(projectName, new RefRight.RefPattern(refPattern),
-            categoryId, group.getId());
-    RefRight rr = db.refRights().get(key);
-    if (rr == null) {
-      rr = new RefRight(key);
-      rr.setMinValue(min);
-      rr.setMaxValue(max);
-      db.refRights().insert(Collections.singleton(rr));
-    } else {
-      rr.setMinValue(min);
-      rr.setMaxValue(max);
-      db.refRights().update(Collections.singleton(rr));
-    }
-    projectCache.evictAll();
-    return projectDetailFactory.create(projectName).call();
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
new file mode 100644
index 0000000..ffe44a3
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
@@ -0,0 +1,206 @@
+// Copyright (C) 2010 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.httpd.rpc.project;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.ProjectAccess;
+import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gwtorm.client.OrmConcurrencyException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+class ChangeProjectAccess extends Handler<ProjectAccess> {
+  interface Factory {
+    ChangeProjectAccess create(@Assisted Project.NameKey projectName,
+        @Assisted ObjectId base, @Assisted List<AccessSection> sectionList,
+        @Nullable @Assisted String message);
+  }
+
+  private final ProjectAccessFactory.Factory projectAccessFactory;
+  private final ProjectControl.Factory projectControlFactory;
+  private final ProjectCache projectCache;
+  private final GroupCache groupCache;
+  private final MetaDataUpdate.User metaDataUpdateFactory;
+
+  private final Project.NameKey projectName;
+  private final ObjectId base;
+  private List<AccessSection> sectionList;
+  private String message;
+
+  @Inject
+  ChangeProjectAccess(final ProjectAccessFactory.Factory projectAccessFactory,
+      final ProjectControl.Factory projectControlFactory,
+      final ProjectCache projectCache, final GroupCache groupCache,
+      final MetaDataUpdate.User metaDataUpdateFactory,
+
+      @Assisted final Project.NameKey projectName,
+      @Assisted final ObjectId base, @Assisted List<AccessSection> sectionList,
+      @Nullable @Assisted String message) {
+    this.projectAccessFactory = projectAccessFactory;
+    this.projectControlFactory = projectControlFactory;
+    this.projectCache = projectCache;
+    this.groupCache = groupCache;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+
+    this.projectName = projectName;
+    this.base = base;
+    this.sectionList = sectionList;
+    this.message = message;
+  }
+
+  @Override
+  public ProjectAccess call() throws NoSuchProjectException, IOException,
+      ConfigInvalidException, InvalidNameException, NoSuchGroupException,
+      OrmConcurrencyException {
+    final ProjectControl projectControl =
+        projectControlFactory.controlFor(projectName);
+
+    final MetaDataUpdate md;
+    try {
+      md = metaDataUpdateFactory.create(projectName);
+    } catch (RepositoryNotFoundException notFound) {
+      throw new NoSuchProjectException(projectName);
+    }
+    try {
+      ProjectConfig config = ProjectConfig.read(md, base);
+      Set<String> toDelete = scanSectionNames(config);
+
+      for (AccessSection section : mergeSections(sectionList)) {
+        final String name = section.getRefPattern();
+        if (!projectControl.controlForRef(name).isOwner()) {
+          continue;
+        }
+
+        if (name.startsWith(AccessSection.REGEX_PREFIX)) {
+          if (!Repository.isValidRefName(RefControl.shortestExample(name))) {
+            throw new InvalidNameException();
+          }
+
+        } else if (name.equals(AccessSection.ALL)) {
+          // This is a special case we have to allow, it fails below.
+
+        } else if (name.endsWith("/*")) {
+          String prefix = name.substring(0, name.length() - 2);
+          if (!Repository.isValidRefName(prefix)) {
+            throw new InvalidNameException();
+          }
+
+        } else if (!Repository.isValidRefName(name)) {
+          throw new InvalidNameException();
+        }
+
+        for (Permission permission : section.getPermissions()) {
+          for (PermissionRule rule : permission.getRules()) {
+            lookupGroup(rule);
+          }
+        }
+
+        config.replace(section);
+        toDelete.remove(section.getRefPattern());
+      }
+
+      for (String name : toDelete) {
+        if (projectControl.controlForRef(name).isOwner()) {
+          config.remove(config.getAccessSection(name));
+        }
+      }
+
+      if (message != null && !message.isEmpty()) {
+        if (!message.endsWith("\n")) {
+          message += "\n";
+        }
+        md.setMessage(message);
+      } else {
+        md.setMessage("Modify access rules\n");
+      }
+
+      if (config.commit(md)) {
+        projectCache.evict(config.getProject());
+        return projectAccessFactory.create(projectName).call();
+
+      } else {
+        throw new OrmConcurrencyException("Cannot update " + projectName);
+      }
+    } finally {
+      md.close();
+    }
+  }
+
+  private static List<AccessSection> mergeSections(List<AccessSection> src) {
+    Map<String, AccessSection> map = new LinkedHashMap<String, AccessSection>();
+    for (AccessSection section : src) {
+      if (section.getPermissions().isEmpty()) {
+        continue;
+      }
+
+      AccessSection prior = map.get(section.getRefPattern());
+      if (prior != null) {
+        prior.mergeFrom(section);
+      } else {
+        map.put(section.getRefPattern(), section);
+      }
+    }
+    return new ArrayList<AccessSection>(map.values());
+  }
+
+  private static Set<String> scanSectionNames(ProjectConfig config) {
+    Set<String> names = new HashSet<String>();
+    for (AccessSection section : config.getAccessSections()) {
+      names.add(section.getRefPattern());
+    }
+    return names;
+  }
+
+  private void lookupGroup(PermissionRule rule) throws NoSuchGroupException {
+    GroupReference ref = rule.getGroup();
+    if (ref.getUUID() == null) {
+      AccountGroup.NameKey name = new AccountGroup.NameKey(ref.getName());
+      AccountGroup group = groupCache.get(name);
+      if (group == null) {
+        throw new NoSuchGroupException(name);
+      }
+      ref.setUUID(group.getGroupUUID());
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
index 88c85b8f..2b0f856 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
@@ -17,16 +17,21 @@
 import com.google.gerrit.common.data.ProjectDetail;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.client.OrmConcurrencyException;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import java.util.Collections;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
 
 class ChangeProjectSettings extends Handler<ProjectDetail> {
   interface Factory {
@@ -36,8 +41,8 @@
   private final ProjectDetailFactory.Factory projectDetailFactory;
   private final ProjectControl.Factory projectControlFactory;
   private final ProjectCache projectCache;
-  private final ReviewDb db;
-  private final GitRepositoryManager repoManager;
+  private final GitRepositoryManager mgr;
+  private final MetaDataUpdate.User metaDataUpdateFactory;
 
   private final Project update;
 
@@ -45,14 +50,14 @@
   ChangeProjectSettings(
       final ProjectDetailFactory.Factory projectDetailFactory,
       final ProjectControl.Factory projectControlFactory,
-      final ProjectCache projectCache, final ReviewDb db,
-      final GitRepositoryManager grm,
+      final ProjectCache projectCache, final GitRepositoryManager mgr,
+      final MetaDataUpdate.User metaDataUpdateFactory,
       @Assisted final Project update) {
     this.projectDetailFactory = projectDetailFactory;
     this.projectControlFactory = projectControlFactory;
     this.projectCache = projectCache;
-    this.db = db;
-    this.repoManager = grm;
+    this.mgr = mgr;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
 
     this.update = update;
   }
@@ -60,20 +65,34 @@
   @Override
   public ProjectDetail call() throws NoSuchProjectException, OrmException {
     final Project.NameKey projectName = update.getNameKey();
-    final ProjectControl projectControl =
-        projectControlFactory.ownerFor(projectName);
+    projectControlFactory.ownerFor(projectName);
 
-    final Project proj = db.projects().get(projectName);
-    if (proj == null) {
+    final MetaDataUpdate md;
+    try {
+      md = metaDataUpdateFactory.create(projectName);
+    } catch (RepositoryNotFoundException notFound) {
       throw new NoSuchProjectException(projectName);
     }
+    try {
+      // TODO We really should take advantage of the Git commit DAG and
+      // ensure the current version matches the old version the caller read.
+      //
+      ProjectConfig config = ProjectConfig.read(md);
+      config.getProject().copySettingsFrom(update);
 
-    proj.copySettingsFrom(update);
-    db.projects().update(Collections.singleton(proj));
-    projectCache.evict(proj);
-
-    if (!projectControl.getProjectState().isSpecialWildProject()) {
-      repoManager.setProjectDescription(projectName, update.getDescription());
+      md.setMessage("Modified project settings\n");
+      if (config.commit(md)) {
+        mgr.setProjectDescription(projectName, update.getDescription());
+        projectCache.evict(config.getProject());
+      } else {
+        throw new OrmConcurrencyException("Cannot update " + projectName);
+      }
+    } catch (ConfigInvalidException err) {
+      throw new OrmException("Cannot read project " + projectName, err);
+    } catch (IOException err) {
+      throw new OrmException("Cannot update project " + projectName, err);
+    } finally {
+      md.close();
     }
 
     return projectDetailFactory.create(projectName).call();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
deleted file mode 100644
index ae8b98b..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright (C) 2010 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.httpd.rpc.project;
-
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.NoSuchRefException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import java.util.Collections;
-import java.util.Set;
-
-class DeleteRefRights extends Handler<ProjectDetail> {
-  interface Factory {
-    DeleteRefRights create(@Assisted Project.NameKey projectName,
-        @Assisted Set<RefRight.Key> toRemove);
-  }
-
-  private final ProjectDetailFactory.Factory projectDetailFactory;
-  private final ProjectControl.Factory projectControlFactory;
-  private final ProjectCache projectCache;
-  private final ReviewDb db;
-
-  private final Project.NameKey projectName;
-  private final Set<RefRight.Key> toRemove;
-
-  @Inject
-  DeleteRefRights(final ProjectDetailFactory.Factory projectDetailFactory,
-      final ProjectControl.Factory projectControlFactory,
-      final ProjectCache projectCache, final ReviewDb db,
-
-      @Assisted final Project.NameKey projectName,
-      @Assisted final Set<RefRight.Key> toRemove) {
-    this.projectDetailFactory = projectDetailFactory;
-    this.projectControlFactory = projectControlFactory;
-    this.projectCache = projectCache;
-    this.db = db;
-
-    this.projectName = projectName;
-    this.toRemove = toRemove;
-  }
-
-  @Override
-  public ProjectDetail call() throws NoSuchProjectException, OrmException,
-      NoSuchRefException {
-    final ProjectControl projectControl =
-        projectControlFactory.controlFor(projectName);
-
-    for (final RefRight.Key k : toRemove) {
-      if (!projectName.equals(k.getProjectNameKey())) {
-        throw new IllegalArgumentException("All keys must be from same project");
-      }
-      String refPattern = k.getRefPattern();
-      if (refPattern.startsWith("-")) {
-        refPattern = refPattern.substring(1);
-      }
-      if (!projectControl.controlForRef(refPattern).isOwner()) {
-        throw new NoSuchRefException(refPattern);
-      }
-    }
-
-    for (final RefRight.Key k : toRemove) {
-      final RefRight m = db.refRights().get(k);
-      if (m != null) {
-        db.refRights().delete(Collections.singleton(m));
-      }
-    }
-    projectCache.evictAll();
-    return projectDetailFactory.create(projectName).call();
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
new file mode 100644
index 0000000..68b54d1
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -0,0 +1,142 @@
+// 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.httpd.rpc.project;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ProjectAccess;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.WildProjectName;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.RefControl;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+class ProjectAccessFactory extends Handler<ProjectAccess> {
+  interface Factory {
+    ProjectAccessFactory create(@Assisted Project.NameKey name);
+  }
+
+  private final GroupCache groupCache;
+  private final ProjectCache projectCache;
+  private final ProjectControl.Factory projectControlFactory;
+  private final MetaDataUpdate.Server metaDataUpdateFactory;
+  private final Project.NameKey wildProject;
+
+  private final Project.NameKey projectName;
+  private ProjectControl pc;
+
+  @Inject
+  ProjectAccessFactory(final GroupCache groupCache,
+      final ProjectCache projectCache,
+      final ProjectControl.Factory projectControlFactory,
+      final MetaDataUpdate.Server metaDataUpdateFactory,
+      @WildProjectName final Project.NameKey wildProject,
+
+
+      @Assisted final Project.NameKey name) {
+    this.groupCache = groupCache;
+    this.projectCache = projectCache;
+    this.projectControlFactory = projectControlFactory;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.wildProject = wildProject;
+
+    this.projectName = name;
+  }
+
+  @Override
+  public ProjectAccess call() throws NoSuchProjectException, IOException,
+      ConfigInvalidException {
+    pc = open();
+
+    // Load the current configuration from the repository, ensuring its the most
+    // recent version available. If it differs from what was in the project
+    // state, force a cache flush now.
+    //
+    ProjectConfig config;
+    MetaDataUpdate md = metaDataUpdateFactory.create(projectName);
+    try {
+      config = ProjectConfig.read(md);
+
+      if (config.updateGroupNames(groupCache)) {
+        md.setMessage("Update group names\n");
+        if (config.commit(md)) {
+          projectCache.evict(config.getProject());
+          pc = open();
+        }
+      } else if (config.getRevision() != null
+          && !config.getRevision().equals(
+              pc.getProjectState().getConfig().getRevision())) {
+        projectCache.evict(config.getProject());
+        pc = open();
+      }
+    } finally {
+      md.close();
+    }
+
+    List<AccessSection> local = new ArrayList<AccessSection>();
+    Set<String> ownerOf = new HashSet<String>();
+    for (AccessSection section : config.getAccessSections()) {
+      RefControl rc = pc.controlForRef(section.getRefPattern());
+      if (rc.isOwner()) {
+        local.add(section);
+        ownerOf.add(section.getRefPattern());
+      } else if (rc.isVisible()) {
+        local.add(section);
+      }
+    }
+
+    if (ownerOf.isEmpty() && pc.isOwnerAnyRef()) {
+      // Special case: If the section list is empty, this project has no current
+      // access control information. Rely on what ProjectControl determines
+      // is ownership, which probably means falling back to site administrators.
+      ownerOf.add(AccessSection.ALL);
+    }
+
+    final ProjectAccess detail = new ProjectAccess();
+    detail.setRevision(config.getRevision().name());
+    detail.setLocal(local);
+    detail.setOwnerOf(ownerOf);
+
+    if (projectName.equals(wildProject)) {
+      detail.setInheritsFrom(null);
+    } else if (config.getProject().getParent() != null) {
+      detail.setInheritsFrom(config.getProject().getParent());
+    } else {
+      detail.setInheritsFrom(wildProject);
+    }
+
+    return detail;
+  }
+
+  private ProjectControl open() throws NoSuchProjectException {
+    return projectControlFactory.validateFor( //
+        projectName, //
+        ProjectControl.OWNER | ProjectControl.VISIBLE);
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
index 9b06dd9..0f9ffff 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
@@ -14,46 +14,48 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ListBranchesResult;
+import com.google.gerrit.common.data.ProjectAccess;
 import com.google.gerrit.common.data.ProjectAdminService;
 import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.ObjectId;
+
 import java.util.List;
 import java.util.Set;
 
 class ProjectAdminServiceImpl implements ProjectAdminService {
   private final AddBranch.Factory addBranchFactory;
+  private final ChangeProjectAccess.Factory changeProjectAccessFactory;
   private final ChangeProjectSettings.Factory changeProjectSettingsFactory;
   private final DeleteBranches.Factory deleteBranchesFactory;
   private final ListBranches.Factory listBranchesFactory;
   private final VisibleProjects.Factory visibleProjectsFactory;
+  private final ProjectAccessFactory.Factory projectAccessFactory;
   private final ProjectDetailFactory.Factory projectDetailFactory;
-  private final AddRefRight.Factory addRefRightFactory;
-  private final DeleteRefRights.Factory deleteRefRightsFactory;
 
   @Inject
   ProjectAdminServiceImpl(final AddBranch.Factory addBranchFactory,
+      final ChangeProjectAccess.Factory changeProjectAccessFactory,
       final ChangeProjectSettings.Factory changeProjectSettingsFactory,
       final DeleteBranches.Factory deleteBranchesFactory,
       final ListBranches.Factory listBranchesFactory,
       final VisibleProjects.Factory visibleProjectsFactory,
-      final ProjectDetailFactory.Factory projectDetailFactory,
-      final AddRefRight.Factory addRefRightFactory,
-      final DeleteRefRights.Factory deleteRefRightsFactory) {
+      final ProjectAccessFactory.Factory projectAccessFactory,
+      final ProjectDetailFactory.Factory projectDetailFactory) {
     this.addBranchFactory = addBranchFactory;
+    this.changeProjectAccessFactory = changeProjectAccessFactory;
     this.changeProjectSettingsFactory = changeProjectSettingsFactory;
     this.deleteBranchesFactory = deleteBranchesFactory;
     this.listBranchesFactory = listBranchesFactory;
     this.visibleProjectsFactory = visibleProjectsFactory;
+    this.projectAccessFactory = projectAccessFactory;
     this.projectDetailFactory = projectDetailFactory;
-    this.addRefRightFactory = addRefRightFactory;
-    this.deleteRefRightsFactory = deleteRefRightsFactory;
   }
 
   @Override
@@ -68,24 +70,23 @@
   }
 
   @Override
+  public void projectAccess(final Project.NameKey projectName,
+      final AsyncCallback<ProjectAccess> callback) {
+    projectAccessFactory.create(projectName).to(callback);
+  }
+
+  @Override
   public void changeProjectSettings(final Project update,
       final AsyncCallback<ProjectDetail> callback) {
     changeProjectSettingsFactory.create(update).to(callback);
   }
 
   @Override
-  public void deleteRight(final Project.NameKey projectName,
-      final Set<RefRight.Key> toRemove, final AsyncCallback<ProjectDetail> callback) {
-    deleteRefRightsFactory.create(projectName, toRemove).to(callback);
-  }
-
-  @Override
-  public void addRight(final Project.NameKey projectName,
-      final ApprovalCategory.Id categoryId, final String groupName,
-      final String refPattern, final short min, final short max,
-      final AsyncCallback<ProjectDetail> callback) {
-    addRefRightFactory.create(projectName, categoryId, groupName, refPattern,
-        min, max).to(callback);
+  public void changeProjectAccess(Project.NameKey projectName,
+      String baseRevision, String msg, List<AccessSection> sections,
+      AsyncCallback<ProjectAccess> cb) {
+    ObjectId base = ObjectId.fromString(baseRevision);
+    changeProjectAccessFactory.create(projectName, base, sections, msg).to(cb);
   }
 
   @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
index ef632c4..1eb940b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
@@ -14,50 +14,28 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.common.data.InheritedRefRight;
 import com.google.gerrit.common.data.ProjectDetail;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.RefControl;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
 class ProjectDetailFactory extends Handler<ProjectDetail> {
   interface Factory {
     ProjectDetailFactory create(@Assisted Project.NameKey name);
   }
 
-  private final ApprovalTypes approvalTypes;
-  private final GroupCache groupCache;
   private final ProjectControl.Factory projectControlFactory;
 
   private final Project.NameKey projectName;
-  private Map<AccountGroup.Id, AccountGroup> groups;
 
   @Inject
-  ProjectDetailFactory(final ApprovalTypes approvalTypes,
-      final GroupCache groupCache,
-      final ProjectControl.Factory projectControlFactory,
+  ProjectDetailFactory(final ProjectControl.Factory projectControlFactory,
 
       @Assisted final Project.NameKey name) {
-    this.approvalTypes = approvalTypes;
-    this.groupCache = groupCache;
     this.projectControlFactory = projectControlFactory;
 
     this.projectName = name;
@@ -72,88 +50,13 @@
     final ProjectDetail detail = new ProjectDetail();
     detail.setProject(projectState.getProject());
 
-    groups = new HashMap<AccountGroup.Id, AccountGroup>();
-    final List<InheritedRefRight> refRights = new ArrayList<InheritedRefRight>();
-
-    for (final RefRight r : projectState.getInheritedRights()) {
-      RefControl rc = pc.controlForRef(r.getRefPattern());
-      boolean isOwner = rc.isOwner();
-
-      if (!isOwner && !rc.isVisible()) {
-        continue;
-      }
-
-      InheritedRefRight refRight = new InheritedRefRight(r, true, isOwner);
-      if (!refRights.contains(refRight)) {
-        refRights.add(refRight);
-        wantGroup(r.getAccountGroupId());
-      }
-    }
-
-    for (final RefRight r : projectState.getLocalRights()) {
-      RefControl rc = pc.controlForRef(r.getRefPattern());
-      boolean isOwner = rc.isOwner();
-
-      if (!isOwner && !rc.isVisible()) {
-        continue;
-      }
-
-      refRights.add(new InheritedRefRight(r, false, isOwner));
-      wantGroup(r.getAccountGroupId());
-    }
-
-    loadGroups();
-
-    Collections.sort(refRights, new Comparator<InheritedRefRight>() {
-      @Override
-      public int compare(final InheritedRefRight a, final InheritedRefRight b) {
-        final RefRight right1 = a.getRight();
-        final RefRight right2 = b.getRight();
-        int rc = categoryOf(right1).compareTo(categoryOf(right2));
-        if (rc == 0) {
-          rc = right1.getRefPattern().compareTo(right2.getRefPattern());
-        }
-        if (rc == 0) {
-          rc = groupOf(right1).compareTo(groupOf(right2));
-        }
-        return rc;
-      }
-
-      private String categoryOf(final RefRight r) {
-        final ApprovalType type =
-            approvalTypes.getApprovalType(r.getApprovalCategoryId());
-        if (type == null) {
-          return r.getApprovalCategoryId().get();
-        }
-        return type.getCategory().getName();
-      }
-
-      private String groupOf(final RefRight r) {
-        return groups.get(r.getAccountGroupId()).getName();
-      }
-    });
-
     final boolean userIsOwner = pc.isOwner();
     final boolean userIsOwnerAnyRef = pc.isOwnerAnyRef();
 
-    detail.setRights(refRights);
-    detail.setGroups(groups);
     detail.setCanModifyAccess(userIsOwnerAnyRef);
     detail.setCanModifyAgreements(userIsOwner);
     detail.setCanModifyDescription(userIsOwner);
     detail.setCanModifyMergeType(userIsOwner);
     return detail;
   }
-
-  private void wantGroup(final AccountGroup.Id id) {
-    groups.put(id, null);
-  }
-
-  private void loadGroups() {
-    final Set<AccountGroup.Id> toGet = groups.keySet();
-    groups = new HashMap<AccountGroup.Id, AccountGroup>();
-    for (AccountGroup.Id groupId : toGet) {
-      groups.put(groupId, groupCache.get(groupId));
-    }
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
index 7932c79..68b3625 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
@@ -29,12 +29,12 @@
       @Override
       protected void configure() {
         factory(AddBranch.Factory.class);
-        factory(AddRefRight.Factory.class);
+        factory(ChangeProjectAccess.Factory.class);
         factory(ChangeProjectSettings.Factory.class);
         factory(DeleteBranches.Factory.class);
-        factory(DeleteRefRights.Factory.class);
         factory(ListBranches.Factory.class);
         factory(VisibleProjects.Factory.class);
+        factory(ProjectAccessFactory.Factory.class);
         factory(ProjectDetailFactory.Factory.class);
       }
     });
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjects.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjects.java
index 2588350..31ba77e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjects.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjects.java
@@ -17,11 +17,9 @@
 
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 
 import java.util.ArrayList;
@@ -35,33 +33,26 @@
   }
 
   private final ProjectControl.Factory projectControlFactory;
-  private final CurrentUser user;
-  private final ReviewDb db;
+  private final ProjectCache projectCache;
 
   @Inject
   VisibleProjects(final ProjectControl.Factory projectControlFactory,
-      final CurrentUser user, final ReviewDb db) {
+       final ProjectCache projectCache) {
     this.projectControlFactory = projectControlFactory;
-    this.user = user;
-    this.db = db;
+    this.projectCache = projectCache;
   }
 
   @Override
-  public List<Project> call() throws OrmException {
-    final List<Project> result;
-    if (user.isAdministrator()) {
-      result = db.projects().all().toList();
-    } else {
-      result = new ArrayList<Project>();
-      for (Project p : db.projects().all().toList()) {
-        try {
-          ProjectControl c = projectControlFactory.controlFor(p.getNameKey());
-          if (c.isVisible() || c.isOwner()) {
-            result.add(p);
-          }
-        } catch (NoSuchProjectException e) {
-          continue;
+  public List<Project> call() {
+    List<Project> result = new ArrayList<Project>();
+    for (Project.NameKey p : projectCache.all()) {
+      try {
+        ProjectControl c = projectControlFactory.controlFor(p);
+        if (c.isVisible() || c.isOwner()) {
+          result.add(c.getProject());
         }
+      } catch (NoSuchProjectException e) {
+        continue;
       }
     }
     Collections.sort(result, new Comparator<Project>() {
diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml
index 3c9c979..a5b0159 100644
--- a/gerrit-launcher/pom.xml
+++ b/gerrit-launcher/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-launcher</artifactId>
diff --git a/gerrit-main/pom.xml b/gerrit-main/pom.xml
index a46aa4f..f630be64 100644
--- a/gerrit-main/pom.xml
+++ b/gerrit-main/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-main</artifactId>
diff --git a/gerrit-patch-commonsnet/pom.xml b/gerrit-patch-commonsnet/pom.xml
index 5bbae03..738d6ce 100644
--- a/gerrit-patch-commonsnet/pom.xml
+++ b/gerrit-patch-commonsnet/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-commonsnet</artifactId>
diff --git a/gerrit-patch-jgit/pom.xml b/gerrit-patch-jgit/pom.xml
index f30eace..f2d1cfd 100644
--- a/gerrit-patch-jgit/pom.xml
+++ b/gerrit-patch-jgit/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-jgit</artifactId>
@@ -49,4 +49,20 @@
       <scope>provided</scope>
     </dependency>
   </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
 </project>
diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml
index f658f52..54f16a7 100644
--- a/gerrit-pgm/pom.xml
+++ b/gerrit-pgm/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-pgm</artifactId>
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
index 71512f2..94cad95 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
@@ -24,8 +24,6 @@
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.GroupCacheImpl;
 import com.google.gerrit.server.cache.CachePool;
@@ -47,7 +45,6 @@
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.lib.ThreadSafeProgressMonitor;
@@ -94,13 +91,10 @@
     gitInjector = dbInjector.createChildInjector(new AbstractModule() {
       @Override
       protected void configure() {
-        bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class);
         bind(ApprovalTypes.class).toProvider(ApprovalTypesProvider.class).in(
             Scopes.SINGLETON);
         bind(String.class).annotatedWith(CanonicalWebUrl.class)
             .toProvider(CanonicalWebUrlProvider.class).in(Scopes.SINGLETON);
-        bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class)
-            .toProvider(GerritPersonIdentProvider.class).in(Scopes.SINGLETON);
         bind(CachePool.class);
 
         install(AccountCacheImpl.module());
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
index 7127027..68ac33d 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
@@ -31,9 +31,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.git.GitProjectImporter;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.schema.SchemaUpdater;
 import com.google.gerrit.server.schema.UpdateUI;
 import com.google.gerrit.server.util.HostPlatform;
@@ -62,9 +60,6 @@
   @Option(name = "--batch", usage = "Batch mode; skip interactive prompting")
   private boolean batchMode;
 
-  @Option(name = "--import-projects", usage = "Import git repositories as projects")
-  private boolean importProjects;
-
   @Option(name = "--no-auto-start", usage = "Don't automatically start daemon after init")
   private boolean noAutoStart;
 
@@ -73,7 +68,6 @@
     ErrorLogFile.errorOnlyConsole();
 
     final SiteInit init = createSiteInit();
-    init.flags.importProjects = importProjects;
     init.flags.autoStart = !noAutoStart && init.site.isNew;
 
     final SiteRun run;
@@ -83,7 +77,6 @@
 
       run = createSiteRun(init);
       run.upgradeSchema();
-      run.importGit();
     } catch (Exception failure) {
       if (init.flags.deleteOnFailure) {
         recursiveDelete(getSitePath());
@@ -166,7 +159,6 @@
     final SchemaUpdater schemaUpdater;
     final SchemaFactory<ReviewDb> schema;
     final GitRepositoryManager repositoryManager;
-    final GitProjectImporter gitProjectImporter;
     final Browser browser;
 
     @Inject
@@ -174,14 +166,13 @@
         final SchemaUpdater schemaUpdater,
         final SchemaFactory<ReviewDb> schema,
         final GitRepositoryManager repositoryManager,
-        final GitProjectImporter gitProjectImporter, final Browser browser) {
+        final Browser browser) {
       this.ui = ui;
       this.site = site;
       this.flags = flags;
       this.schemaUpdater = schemaUpdater;
       this.schema = schema;
       this.repositoryManager = repositoryManager;
-      this.gitProjectImporter = gitProjectImporter;
       this.browser = browser;
     }
 
@@ -241,23 +232,6 @@
       }
     }
 
-    void importGit() throws OrmException, IOException {
-      if (flags.importProjects) {
-        gitProjectImporter.run(new GitProjectImporter.Messages() {
-          @Override
-          public void info(String msg) {
-            System.err.println(msg);
-            System.err.flush();
-          }
-
-          @Override
-          public void warning(String msg) {
-            info(msg);
-          }
-        });
-      }
-    }
-
     void start() throws Exception {
       if (flags.autoStart) {
         if (HostPlatform.isWin32()) {
@@ -317,9 +291,6 @@
       protected void configure() {
         bind(ConsoleUI.class).toInstance(init.ui);
         bind(InitFlags.class).toInstance(init.flags);
-
-        bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class);
-        bind(GitProjectImporter.class);
       }
     });
     return createDbInjector(SINGLE_USER).createChildInjector(modules);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
index beeed24..45f560a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
@@ -17,7 +17,6 @@
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
 import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
@@ -26,7 +25,6 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
@@ -56,7 +54,6 @@
   private List<Change> todo;
 
   private Injector dbInjector;
-  private Injector gitInjector;
 
   @Inject
   private TrackingFooters footers;
@@ -74,17 +71,9 @@
     }
 
     dbInjector = createDbInjector(MULTI_USER);
-    gitInjector = dbInjector.createChildInjector(new LifecycleModule() {
-      @Override
-      protected void configure() {
-        bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class);
-        listener().to(LocalDiskRepositoryManager.Lifecycle.class);
-      }
-    });
-
-    manager.add(dbInjector, gitInjector);
+    manager.add(dbInjector);
     manager.start();
-    gitInjector.injectMembers(this);
+    dbInjector.injectMembers(this);
 
     final ReviewDb db = database.open();
     try {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
index 992c616..5d71b48 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
@@ -30,9 +30,6 @@
   /** Recursively delete the site path if initialization fails. */
   public boolean deleteOnFailure;
 
-  /** Run the Git project importer after initialization. */
-  public boolean importProjects;
-
   /** Run the daemon (and open the web UI in a browser) after initialization. */
   public boolean autoStart;
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
index 2d95577..f0cd31f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
@@ -25,14 +25,11 @@
 /** Initialize the GitRepositoryManager configuration section. */
 @Singleton
 class InitGitManager implements InitStep {
-  private final InitFlags flags;
   private final ConsoleUI ui;
   private final Section gerrit;
 
   @Inject
-  InitGitManager(final InitFlags flags, final ConsoleUI ui,
-      final Section.Factory sections) {
-    this.flags = flags;
+  InitGitManager(final ConsoleUI ui, final Section.Factory sections) {
     this.ui = ui;
     this.gerrit = sections.get("gerrit");
   }
@@ -44,11 +41,7 @@
     if (d == null) {
       throw die("gerrit.basePath is required");
     }
-    if (d.exists()) {
-      if (!flags.importProjects && d.list() != null && d.list().length > 0) {
-        flags.importProjects = ui.yesno(true, "Import existing repositories");
-      }
-    } else if (!d.mkdirs()) {
+    if (!d.exists() && !d.mkdirs()) {
       throw die("Cannot create " + d);
     }
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
index 8e3306b..340168c 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DatabaseModule;
+import com.google.gerrit.server.schema.SchemaModule;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.AbstractModule;
 import com.google.inject.CreationException;
@@ -162,6 +163,7 @@
     });
     modules.add(new GerritServerConfigModule());
     modules.add(new DatabaseModule());
+    modules.add(new SchemaModule());
 
     try {
       return Guice.createInjector(PRODUCTION, modules);
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
index 9aba73e..4538bce 100755
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
@@ -54,70 +54,22 @@
 
 get_config() {
   if test -f "$GERRIT_CONFIG" ; then
-    if type git >/dev/null 2>&1 ; then
-      if test "x$1" = x--int ; then
-        # Git might not be able to expand "8g" properly.  If it gives
-        # us 0 back retry for the raw string and expand ourselves.
-        #
-        n=`git config --file "$GERRIT_CONFIG" --int "$2"`
-        if test x0 = "x$n" ; then
-          n=`git config --file "$GERRIT_CONFIG" --get "$2"`
-          case "$n" in
-          *g) n=`expr ${n%%g} \* 1024`m ;;
-          *k) n=`expr ${n%%k} \* 1024` ;;
-          *)  : ;;
-          esac
-        fi
-        echo "$n"
-      else
-        git config --file "$GERRIT_CONFIG" $1 "$2"
-      fi
-
-    else
-      # This is a very crude parser for the git configuration file.
-      # Its not perfect but it can at least pull some basic values
-      # from a reasonably standard format.
+    if test "x$1" = x--int ; then
+      # Git might not be able to expand "8g" properly.  If it gives
+      # us 0 back retry for the raw string and expand ourselves.
       #
-      s=`echo "$2" | cut -d. -f1`
-      k=`echo "$2" | cut -d. -f2`
-      i=0
-      while read n ; do
+      n=`git config --file "$GERRIT_CONFIG" --int "$2"`
+      if test x0 = "x$n" ; then
+        n=`git config --file "$GERRIT_CONFIG" --get "$2"`
         case "$n" in
-        '['$s']') i=1 ;;
-        '['*']' ) i=0 ;;
+        *g) n=`expr ${n%%g} \* 1024`m ;;
+        *k) n=`expr ${n%%k} \* 1024` ;;
+        *)  : ;;
         esac
-        test $i || continue
-
-        case "$n" in
-        *[' 	']$k[' 	']*=*) : ;;
-        [' 	']$k=*) : ;;
-        $k[' 	']*=*) : ;;
-        $k=*) : ;;
-        *) continue ;;
-        esac
-
-        n=${n#*=}
-
-        if test "x$1" = x--bool ; then
-          case "$n" in
-          true|on|1|yes) n=true ;;
-          false|off|0|no) n=false ;;
-          *)
-            echo >&2 "error: $2=$n not supported, assuming false."
-            n=false
-            ;;
-          esac
-        fi
-
-        if test "x$1" = x--int ; then
-          case "$n" in
-          *g) n=`expr ${n%%g} \* 1024`m ;;
-          *k) n=`expr ${n%%k} \* 1024` ;;
-          *)  : ;;
-          esac
-        fi
-        echo "$n" 
-      done <"$GERRIT_CONFIG"
+      fi
+      echo "$n"
+    else
+      git config --file "$GERRIT_CONFIG" $1 "$2"
     fi
   fi
 }
@@ -174,6 +126,16 @@
 GERRIT_INSTALL_TRACE_FILE=etc/gerrit.config
 
 ##################################################
+# No git in PATH? Needed for gerrit.confg parsing
+##################################################
+if type git >/dev/null 2>&1 ; then
+  : OK
+else
+  echo >&2 "** ERROR: Cannot find git in PATH"
+  exit 1
+fi
+
+##################################################
 # Try to determine GERRIT_SITE if not set
 ##################################################
 if test -z "$GERRIT_SITE" ; then
diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml
index 060ffdd..3ac4deb 100644
--- a/gerrit-prettify/pom.xml
+++ b/gerrit-prettify/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-prettify</artifactId>
@@ -56,4 +56,20 @@
       <scope>provided</scope>
     </dependency>
   </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
 </project>
diff --git a/gerrit-reviewdb/pom.xml b/gerrit-reviewdb/pom.xml
index d81b068..8c43204 100644
--- a/gerrit-reviewdb/pom.xml
+++ b/gerrit-reviewdb/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-reviewdb</artifactId>
@@ -38,4 +38,20 @@
       <artifactId>gwtorm</artifactId>
     </dependency>
   </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
 </project>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreference.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreference.java
index 38a2359..ccfdbd5 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreference.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreference.java
@@ -102,6 +102,9 @@
   @Column(id = 11)
   protected boolean skipUncommented;
 
+  @Column(id = 12)
+  protected boolean expandAllComments;
+
   protected AccountDiffPreference() {
   }
 
@@ -120,6 +123,7 @@
     this.showTabs = p.showTabs;
     this.skipDeleted = p.skipDeleted;
     this.skipUncommented = p.skipUncommented;
+    this.expandAllComments = p.expandAllComments;
     this.context = p.context;
   }
 
@@ -209,4 +213,12 @@
   public void setSkipUncommented(boolean skip) {
     skipUncommented = skip;
   }
+
+  public boolean isExpandAllComments() {
+    return expandAllComments;
+  }
+
+  public void setExpandAllComments(boolean expand) {
+    expandAllComments = expand;
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroup.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroup.java
index d2aceaa..5d8a4b9 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroup.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroup.java
@@ -46,6 +46,39 @@
     }
   }
 
+  /** Globally unique identifier. */
+  public static class UUID extends
+      StringKey<com.google.gwtorm.client.Key<?>> {
+    private static final long serialVersionUID = 1L;
+
+    @Column(id = 1, length = 40)
+    protected String uuid;
+
+    protected UUID() {
+    }
+
+    public UUID(final String n) {
+      uuid = n;
+    }
+
+    @Override
+    public String get() {
+      return uuid;
+    }
+
+    @Override
+    protected void set(String newValue) {
+      uuid = newValue;
+    }
+
+    /** Parse an AccountGroup.UUID out of a string representation. */
+    public static UUID parse(final String str) {
+      final UUID r = new UUID();
+      r.fromString(str);
+      return r;
+    }
+  }
+
   /** Distinguished name, within organization directory server. */
   public static class ExternalNameKey extends
       StringKey<com.google.gwtorm.client.Key<?>> {
@@ -140,6 +173,18 @@
     LDAP;
   }
 
+  /** Common UUID assigned to the "Project Owners" placeholder group. */
+  public static final AccountGroup.UUID PROJECT_OWNERS =
+      new AccountGroup.UUID("global:Project-Owners");
+
+  /** Common UUID assigned to the "Anonymous Users" group. */
+  public static final AccountGroup.UUID ANONYMOUS_USERS =
+      new AccountGroup.UUID("global:Anonymous-Users");
+
+  /** Common UUID assigned to the "Registered Users" group. */
+  public static final AccountGroup.UUID REGISTERED_USERS =
+      new AccountGroup.UUID("global:Registered-Users");
+
   /** Unique name of this group within the system. */
   @Column(id = 1)
   protected NameKey name;
@@ -176,15 +221,20 @@
   @Column(id = 8)
   protected boolean emailOnlyAuthors;
 
+  /** Globally unique identifier name for this group. */
+  @Column(id = 9)
+  protected UUID groupUUID;
+
   protected AccountGroup() {
   }
 
   public AccountGroup(final AccountGroup.NameKey newName,
-      final AccountGroup.Id newId) {
+      final AccountGroup.Id newId, final AccountGroup.UUID uuid) {
     name = newName;
     groupId = newId;
     ownerGroupId = groupId;
     visibleToAll = false;
+    groupUUID = uuid;
     setType(Type.INTERNAL);
   }
 
@@ -251,4 +301,12 @@
   public void setEmailOnlyAuthors(boolean emailOnlyAuthors) {
     this.emailOnlyAuthors = emailOnlyAuthors;
   }
+
+  public AccountGroup.UUID getGroupUUID() {
+    return groupUUID;
+  }
+
+  public void setGroupUUID(AccountGroup.UUID uuid) {
+    groupUUID = uuid;
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAccess.java
index 2530654..7eb7ed2 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAccess.java
@@ -25,6 +25,9 @@
   @PrimaryKey("groupId")
   AccountGroup get(AccountGroup.Id id) throws OrmException;
 
+  @Query("WHERE groupUUID = ?")
+  ResultSet<AccountGroup> byUUID(AccountGroup.UUID uuid) throws OrmException;
+
   @Query("WHERE externalName = ?")
   ResultSet<AccountGroup> byExternalName(AccountGroup.ExternalNameKey name)
       throws OrmException;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategory.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategory.java
index 29ea97e..d7e5024 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategory.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategory.java
@@ -24,33 +24,6 @@
   public static final ApprovalCategory.Id SUBMIT =
       new ApprovalCategory.Id("SUBM");
 
-  /** Id of the special "Read" action (and category). */
-  public static final ApprovalCategory.Id READ =
-      new ApprovalCategory.Id("READ");
-
-  /** Id of the special "Own" category; manages a project. */
-  public static final ApprovalCategory.Id OWN = new ApprovalCategory.Id("OWN");
-
-  /** Id of the special "Push Annotated Tag" action (and category). */
-  public static final ApprovalCategory.Id PUSH_TAG =
-      new ApprovalCategory.Id("pTAG");
-  public static final short PUSH_TAG_SIGNED = 1;
-  public static final short PUSH_TAG_ANNOTATED = 2;
-
-  /** Id of the special "Push Branch" action (and category). */
-  public static final ApprovalCategory.Id PUSH_HEAD =
-      new ApprovalCategory.Id("pHD");
-  public static final short PUSH_HEAD_UPDATE = 1;
-  public static final short PUSH_HEAD_CREATE = 2;
-  public static final short PUSH_HEAD_REPLACE = 3;
-
-  /** Id of the special "Forge Identity" category. */
-  public static final ApprovalCategory.Id FORGE_IDENTITY =
-      new ApprovalCategory.Id("FORG");
-  public static final short FORGE_AUTHOR = 1;
-  public static final short FORGE_COMMITTER = 2;
-  public static final short FORGE_SERVER = 3;
-
   public static class Id extends StringKey<Key<?>> {
     private static final long serialVersionUID = 1L;
 
@@ -73,15 +46,6 @@
     protected void set(String newValue) {
       id = newValue;
     }
-
-    /** True if the right can be assigned on the wild project. */
-    public boolean canBeOnWildProject() {
-      if (OWN.equals(this)) {
-        return false;
-      } else {
-        return true;
-      }
-    }
   }
 
   /** Internal short unique identifier for this category. */
@@ -96,16 +60,7 @@
   @Column(id = 3, length = 4, notNull = false)
   protected String abbreviatedName;
 
-  /**
-   * Order of this category within the Approvals table when presented.
-   * <p>
-   * If < 0 (e.g. -1) this category is not shown in the Approvals table but is
-   * instead considered to be an action that the user might be able to perform,
-   * e.g. "Submit".
-   * <p>
-   * If >= 0 this category is shown in the Approvals table, sorted along with
-   * its siblings by <code>position, name</code>.
-   */
+  /** Order of this category within the Approvals table when presented. */
   @Column(id = 4)
   protected short position;
 
@@ -117,6 +72,9 @@
   @Column(id = 6)
   protected boolean copyMinScore;
 
+  /** Computed name derived from {@link #name}. */
+  protected String labelName;
+
   protected ApprovalCategory() {
   }
 
@@ -136,6 +94,26 @@
 
   public void setName(final String n) {
     name = n;
+    labelName = null;
+  }
+
+  /** Clean version of {@link #getName()}, e.g. "Code Review" is "Code-Review". */
+  public String getLabelName() {
+    if (labelName == null) {
+      StringBuilder r = new StringBuilder();
+      for (int i = 0; i < name.length(); i++) {
+        char c = name.charAt(i);
+        if (('0' <= c && c <= '9') //
+            || ('a' <= c && c <= 'z') //
+            || ('A' <= c && c <= 'Z')) {
+          r.append(c);
+        } else if (c == ' ') {
+          r.append('-');
+        }
+      }
+      labelName = r.toString();
+    }
+    return labelName;
   }
 
   public String getAbbreviatedName() {
@@ -154,14 +132,6 @@
     position = p;
   }
 
-  public boolean isAction() {
-    return position < 0;
-  }
-
-  public boolean isRange() {
-    return !isAction();
-  }
-
   public String getFunctionName() {
     return functionName;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java
index 409547a..49f3333 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java
@@ -21,7 +21,7 @@
 public final class Project {
   /** Project name key */
   public static class NameKey extends
-      StringKey<com.google.gwtorm.client.Key<?>> {
+      StringKey<com.google.gwtorm.client.Key<?>> implements Comparable<NameKey> {
     private static final long serialVersionUID = 1L;
 
     @Column(id = 1)
@@ -44,6 +44,11 @@
       name = newValue;
     }
 
+    @Override
+    public int compareTo(NameKey other) {
+      return get().compareTo(other.get());
+    }
+
     /** Parse a Project.NameKey out of a string representation. */
     public static NameKey parse(final String str) {
       final NameKey r = new NameKey();
@@ -53,65 +58,37 @@
   }
 
   public static enum SubmitType {
-    FAST_FORWARD_ONLY('F'),
+    FAST_FORWARD_ONLY,
 
-    MERGE_IF_NECESSARY('M'),
+    MERGE_IF_NECESSARY,
 
-    MERGE_ALWAYS('A'),
+    MERGE_ALWAYS,
 
-    CHERRY_PICK('C');
-
-    private final char code;
-
-    private SubmitType(final char c) {
-      code = c;
-    }
-
-    public char getCode() {
-      return code;
-    }
-
-    public static SubmitType forCode(final char c) {
-      for (final SubmitType s : SubmitType.values()) {
-        if (s.code == c) {
-          return s;
-        }
-      }
-      return null;
-    }
+    CHERRY_PICK;
   }
 
-  @Column(id = 1)
   protected NameKey name;
 
-  @Column(id = 2, length = Integer.MAX_VALUE, notNull = false)
   protected String description;
 
-  @Column(id = 3)
   protected boolean useContributorAgreements;
 
-  @Column(id = 4)
   protected boolean useSignedOffBy;
 
-  @Column(id = 5)
-  protected char submitType;
+  protected SubmitType submitType;
 
-  @Column(id = 6, notNull = false, name = "parent_name")
   protected NameKey parent;
 
-  @Column(id = 7)
   protected boolean requireChangeID;
 
-  @Column(id = 8)
   protected boolean useContentMerge;
 
   protected Project() {
   }
 
-  public Project(final Project.NameKey newName) {
-    name = newName;
-    useContributorAgreements = true;
-    setSubmitType(SubmitType.MERGE_IF_NECESSARY);
+  public Project(Project.NameKey nameKey) {
+    name = nameKey;
+    submitType = SubmitType.MERGE_IF_NECESSARY;
   }
 
   public Project.NameKey getNameKey() {
@@ -119,7 +96,7 @@
   }
 
   public String getName() {
-    return name.get();
+    return name != null ? name.get() : null;
   }
 
   public String getDescription() {
@@ -163,11 +140,11 @@
   }
 
   public SubmitType getSubmitType() {
-    return SubmitType.forCode(submitType);
+    return submitType;
   }
 
   public void setSubmitType(final SubmitType type) {
-    submitType = type.getCode();
+    submitType = type;
   }
 
   public void copySettingsFrom(final Project update) {
@@ -183,7 +160,11 @@
     return parent;
   }
 
-  public void setParent(final Project.NameKey parentProjectName) {
-      parent = parentProjectName;
+  public String getParentName() {
+    return parent != null ? parent.get() : null;
+  }
+
+  public void setParentName(String n) {
+    parent = n != null ? new NameKey(n) : null;
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ProjectAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ProjectAccess.java
deleted file mode 100644
index b9adada..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ProjectAccess.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2008 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.reviewdb;
-
-import com.google.gwtorm.client.Access;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.PrimaryKey;
-import com.google.gwtorm.client.Query;
-import com.google.gwtorm.client.ResultSet;
-
-public interface ProjectAccess extends Access<Project, Project.NameKey> {
-  @PrimaryKey("name")
-  Project get(Project.NameKey name) throws OrmException;
-
-  @Query("ORDER BY name")
-  ResultSet<Project> all() throws OrmException;
-
-  @Query("WHERE name.name >= ? AND name.name <= ? ORDER BY name LIMIT ?")
-  ResultSet<Project> suggestByName(String nameA, String nameB, int limit)
-      throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java
deleted file mode 100644
index 97ee219..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java
+++ /dev/null
@@ -1,235 +0,0 @@
-// Copyright (C) 2010 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.reviewdb;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.CompoundKey;
-import com.google.gwtorm.client.StringKey;
-
-import java.util.Comparator;
-
-/** Grant to use an {@link ApprovalCategory} in the scope of a git ref. */
-public final class RefRight {
-  /** Pattern that matches all references in a project. */
-  public static final String ALL = "refs/*";
-
-  /** Prefix that triggers a regular expression pattern. */
-  public static final String REGEX_PREFIX = "^";
-
-  public static class RefPattern extends
-      StringKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected String pattern;
-
-    protected RefPattern() {
-    }
-
-    public RefPattern(final String pattern) {
-      this.pattern = pattern;
-    }
-
-    @Override
-    public String get() {
-      return pattern;
-    }
-
-    @Override
-    protected void set(String pattern) {
-      this.pattern = pattern;
-    }
-  }
-
-  public static class Key extends CompoundKey<Project.NameKey> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected Project.NameKey projectName;
-
-    @Column(id = 2)
-    protected RefPattern refPattern;
-
-    @Column(id = 3)
-    protected ApprovalCategory.Id categoryId;
-
-    @Column(id = 4)
-    protected AccountGroup.Id groupId;
-
-    protected Key() {
-      projectName = new Project.NameKey();
-      refPattern = new RefPattern();
-      categoryId = new ApprovalCategory.Id();
-      groupId = new AccountGroup.Id();
-    }
-
-    public Key(final Project.NameKey projectName, final RefPattern refPattern,
-        final ApprovalCategory.Id categoryId, final AccountGroup.Id groupId) {
-      this.projectName = projectName;
-      this.refPattern = refPattern;
-      this.categoryId = categoryId;
-      this.groupId = groupId;
-    }
-
-    @Override
-    public Project.NameKey getParentKey() {
-      return projectName;
-    }
-
-    public Project.NameKey getProjectNameKey() {
-      return projectName;
-    }
-
-    public String getRefPattern() {
-      return refPattern.get();
-    }
-
-    public void setGroupId(AccountGroup.Id groupId) {
-      this.groupId = groupId;
-    }
-
-    @Override
-    public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {refPattern, categoryId,
-          groupId};
-    }
-  }
-
-  @Column(id = 1, name = Column.NONE)
-  protected Key key;
-
-  @Column(id = 2)
-  protected short minValue;
-
-  @Column(id = 3)
-  protected short maxValue;
-
-  protected RefRight() {
-  }
-
-  public RefRight(RefRight.Key key) {
-    this.key = key;
-  }
-
-  public RefRight(final RefRight refRight, final AccountGroup.Id groupId) {
-    this(new RefRight.Key(refRight.getKey().projectName,
-        refRight.getKey().refPattern, refRight.getKey().categoryId, groupId));
-    setMinValue(refRight.getMinValue());
-    setMaxValue(refRight.getMaxValue());
-  }
-
-  public RefRight.Key getKey() {
-    return key;
-  }
-
-  public String getRefPattern() {
-    if (isExclusive()) {
-      return key.refPattern.get().substring(1);
-    }
-    return key.refPattern.get();
-  }
-
-  public String getRefPatternForDisplay() {
-    return key.refPattern.get();
-  }
-
-  public Project.NameKey getProjectNameKey() {
-    return getKey().getProjectNameKey();
-  }
-
-  public boolean isExclusive() {
-    return key.refPattern.get().startsWith("-");
-  }
-
-  public ApprovalCategory.Id getApprovalCategoryId() {
-    return key.categoryId;
-  }
-
-  public AccountGroup.Id getAccountGroupId() {
-    return key.groupId;
-  }
-
-  public short getMinValue() {
-    return minValue;
-  }
-
-  public void setMinValue(final short m) {
-    minValue = m;
-  }
-
-  public short getMaxValue() {
-    return maxValue;
-  }
-
-  public void setMaxValue(final short m) {
-    maxValue = m;
-  }
-
-  @Override
-  public String toString() {
-    StringBuilder s = new StringBuilder();
-    s.append("{group :");
-    s.append(getAccountGroupId().get());
-    s.append(", proj :");
-    s.append(getProjectNameKey().get());
-    s.append(", cat :");
-    s.append(getApprovalCategoryId().get());
-    s.append(", pattern :");
-    s.append(getRefPatternForDisplay());
-    s.append(", min :");
-    s.append(getMinValue());
-    s.append(", max :");
-    s.append(getMaxValue());
-    s.append("}");
-    return s.toString();
-  }
-
-  @Override
-  public int hashCode() {
-    return getKey().hashCode();
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o instanceof RefRight) {
-      RefRight a = this;
-      RefRight b = (RefRight) o;
-      return a.getKey().equals(b.getKey())
-          && a.getMinValue() == b.getMinValue()
-          && a.getMaxValue() == b.getMaxValue();
-    }
-    return false;
-  }
-
-  public static final Comparator<RefRight> REF_PATTERN_ORDER =
-      new Comparator<RefRight>() {
-
-    @Override
-    public int compare(RefRight a, RefRight b) {
-      int aLength = a.getRefPattern().length();
-      int bLength = b.getRefPattern().length();
-      if (bLength == aLength) {
-        ApprovalCategory.Id aCat = a.getApprovalCategoryId();
-        ApprovalCategory.Id bCat = b.getApprovalCategoryId();
-        if (aCat.get().equals(bCat.get())) {
-          return a.getRefPattern().compareTo(b.getRefPattern());
-        }
-        return a.getApprovalCategoryId().get()
-            .compareTo(b.getApprovalCategoryId().get());
-      }
-      return bLength - aLength;
-    }
-  };
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRightAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRightAccess.java
deleted file mode 100644
index a42ff2c..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRightAccess.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2010 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.reviewdb;
-
-import com.google.gwtorm.client.Access;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.PrimaryKey;
-import com.google.gwtorm.client.Query;
-import com.google.gwtorm.client.ResultSet;
-
-public interface RefRightAccess extends Access<RefRight, RefRight.Key> {
-  @PrimaryKey("key")
-  RefRight get(RefRight.Key refRight) throws OrmException;
-
-  @Query("WHERE key.projectName = ?")
-  ResultSet<RefRight> byProject(Project.NameKey project) throws OrmException;
-
-  @Query("WHERE key.categoryId = ? AND key.groupId = ?")
-  ResultSet<RefRight> byCategoryGroup(ApprovalCategory.Id cat,
-      AccountGroup.Id group) throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java
index 49e07ff..b75b91b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java
@@ -24,7 +24,6 @@
  * <p>
  * Root entities that are at the top level of some important data graph:
  * <ul>
- * <li>{@link Project}: Configuration for a single Git repository.</li>
  * <li>{@link Account}: Per-user account registration, preferences, identity.</li>
  * <li>{@link Change}: All review information about a single proposed change.</li>
  * <li>{@link SystemConfig}: Server-wide settings, managed by administrator.</li>
@@ -94,9 +93,6 @@
   AccountPatchReviewAccess accountPatchReviews();
 
   @Relation
-  ProjectAccess projects();
-
-  @Relation
   ChangeAccess changes();
 
   @Relation
@@ -115,9 +111,6 @@
   PatchLineCommentAccess patchComments();
 
   @Relation
-  RefRightAccess refRights();
-
-  @Relation
   TrackingIdAccess trackingIds();
 
   /** Create the next unique id for an {@link Account}. */
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfig.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfig.java
index 6ff23ed..229d173 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfig.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfig.java
@@ -65,6 +65,8 @@
   /** Identity of the administration group; those with full access. */
   @Column(id = 4)
   public AccountGroup.Id adminGroupId;
+  @Column(id = 10)
+  public AccountGroup.UUID adminGroupUUID;
 
   /** Identity of the anonymous group, which permits anyone. */
   @Column(id = 5)
@@ -81,6 +83,8 @@
   /** Identity of the batch users group */
   @Column(id = 8)
   public AccountGroup.Id batchUsersGroupId;
+  @Column(id = 11)
+  public AccountGroup.UUID batchUsersGroupUUID;
 
   /** Identity of the owner group, which permits any project owner. */
   @Column(id = 9)
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql
index d33d24d..9784a4d 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql
@@ -158,14 +158,6 @@
 
 
 -- *********************************************************************
--- RefRightAccess
---    @PrimaryKey covers: byProject
---    covers:             byCategoryGroup
-CREATE INDEX ref_rights_byCatGroup
-ON ref_rights (category_id, group_id);
-
-
--- *********************************************************************
 -- TrackingIdAccess
 --
 CREATE INDEX tracking_ids_byTrkId
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql
index 8e3cead..db6894d 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql
@@ -240,14 +240,6 @@
 
 
 -- *********************************************************************
--- RefRightAccess
---    @PrimaryKey covers: byProject
---    covers:             byCategoryGroup
-CREATE INDEX ref_rights_byCatGroup
-ON ref_rights (category_id, group_id);
-
-
--- *********************************************************************
 -- TrackingIdAccess
 --
 CREATE INDEX tracking_ids_byTrkId
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index e2370c5..74d4f1c 100644
--- a/gerrit-server/pom.xml
+++ b/gerrit-server/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-server</artifactId>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
index 1a4a863..8205946 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -433,8 +433,10 @@
             Entry<ApprovalCategory.Id, ApprovalCategoryValue.Id> approval) {
         ApprovalAttribute a = new ApprovalAttribute();
         a.type = approval.getKey().get();
-        final ApprovalType at = approvalTypes.getApprovalType(approval.getKey());
-        a.description = at.getCategory().getName();
+        ApprovalType at = approvalTypes.byId(approval.getKey());
+        if (at != null) {
+          a.description = at.getCategory().getName();
+        }
         a.value = Short.toString(approval.getValue().get());
         return a;
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
index 1bd2066..3a450ed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
@@ -34,8 +34,8 @@
   }
 
   @Override
-  public Set<AccountGroup.Id> getEffectiveGroups() {
-    return authConfig.getAnonymousGroups();
+  public Set<AccountGroup.UUID> getEffectiveGroups() {
+    return Collections.singleton(AccountGroup.ANONYMOUS_USERS);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 6886d2c..87cded0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.EmailException;
@@ -212,7 +213,7 @@
       final IdentifiedUser user, final String message, final ReviewDb db,
       final AbandonedSender.Factory abandonedSenderFactory,
       final ChangeHookRunner hooks) throws NoSuchChangeException,
-      EmailException, OrmException {
+      InvalidChangeOperationException, EmailException, OrmException {
     final Change.Id changeId = patchSetId.getParentKey();
     final PatchSet patch = db.patchSets().get(patchSetId);
     if (patch == null) {
@@ -245,23 +246,26 @@
       }
     });
 
-    if (updatedChange != null) {
-      db.changeMessages().insert(Collections.singleton(cmsg));
-
-      final List<PatchSetApproval> approvals =
-          db.patchSetApprovals().byChange(changeId).toList();
-      for (PatchSetApproval a : approvals) {
-        a.cache(updatedChange);
-      }
-      db.patchSetApprovals().update(approvals);
-
-      // Email the reviewers
-      final AbandonedSender cm = abandonedSenderFactory.create(updatedChange);
-      cm.setFrom(user.getAccountId());
-      cm.setChangeMessage(cmsg);
-      cm.send();
+    if (updatedChange == null) {
+      throw new InvalidChangeOperationException(
+          "Change is no longer open or patchset is not latest");
     }
 
+    db.changeMessages().insert(Collections.singleton(cmsg));
+
+    final List<PatchSetApproval> approvals =
+        db.patchSetApprovals().byChange(changeId).toList();
+    for (PatchSetApproval a : approvals) {
+      a.cache(updatedChange);
+    }
+    db.patchSetApprovals().update(approvals);
+
+    // Email the reviewers
+    final AbandonedSender cm = abandonedSenderFactory.create(updatedChange);
+    cm.setFrom(user.getAccountId());
+    cm.setChangeMessage(cmsg);
+    cm.send();
+
     hooks.doChangeAbandonedHook(updatedChange, user.getAccount(), message);
   }
 
@@ -371,7 +375,7 @@
       final IdentifiedUser user, final String message, final ReviewDb db,
       final AbandonedSender.Factory abandonedSenderFactory,
       final ChangeHookRunner hooks) throws NoSuchChangeException,
-      EmailException, OrmException {
+      InvalidChangeOperationException, EmailException, OrmException {
     final Change.Id changeId = patchSetId.getParentKey();
     final PatchSet patch = db.patchSets().get(patchSetId);
     if (patch == null) {
@@ -404,23 +408,26 @@
       }
     });
 
-    if (updatedChange != null) {
-      db.changeMessages().insert(Collections.singleton(cmsg));
-
-      final List<PatchSetApproval> approvals =
-          db.patchSetApprovals().byChange(changeId).toList();
-      for (PatchSetApproval a : approvals) {
-        a.cache(updatedChange);
-      }
-      db.patchSetApprovals().update(approvals);
-
-      // Email the reviewers
-      final AbandonedSender cm = abandonedSenderFactory.create(updatedChange);
-      cm.setFrom(user.getAccountId());
-      cm.setChangeMessage(cmsg);
-      cm.send();
+    if (updatedChange == null) {
+      throw new InvalidChangeOperationException(
+          "Change is not abandoned or patchset is not latest");
     }
 
+    db.changeMessages().insert(Collections.singleton(cmsg));
+
+    final List<PatchSetApproval> approvals =
+        db.patchSetApprovals().byChange(changeId).toList();
+    for (PatchSetApproval a : approvals) {
+      a.cache(updatedChange);
+    }
+    db.patchSetApprovals().update(approvals);
+
+    // Email the reviewers
+    final AbandonedSender cm = abandonedSenderFactory.create(updatedChange);
+    cm.setFrom(user.getAccountId());
+    cm.setChangeMessage(cmsg);
+    cm.send();
+
     hooks.doChangeRestoreHook(updatedChange, user.getAccount(), message);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
index 0d8d19a..512de00 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -56,7 +56,7 @@
    *
    * @return active groups for this user.
    */
-  public abstract Set<AccountGroup.Id> getEffectiveGroups();
+  public abstract Set<AccountGroup.UUID> getEffectiveGroups();
 
   /** Set of changes starred by this user. */
   public abstract Set<Change.Id> getStarredChanges();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
index e46957d..5fb5a7f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -43,11 +43,14 @@
 import java.net.MalformedURLException;
 import java.net.SocketAddress;
 import java.net.URL;
+import java.util.AbstractSet;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.LinkedList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Queue;
 import java.util.Set;
@@ -139,6 +142,28 @@
   private static final Logger log =
       LoggerFactory.getLogger(IdentifiedUser.class);
 
+  private static final Set<AccountGroup.UUID> registeredGroups =
+      new AbstractSet<AccountGroup.UUID>() {
+        private final List<AccountGroup.UUID> groups =
+            Collections.unmodifiableList(Arrays.asList(new AccountGroup.UUID[] {
+                AccountGroup.ANONYMOUS_USERS, AccountGroup.REGISTERED_USERS}));
+
+        @Override
+        public boolean contains(Object o) {
+          return groups.contains(o);
+        }
+
+        @Override
+        public Iterator<AccountGroup.UUID> iterator() {
+          return groups.iterator();
+        }
+
+        @Override
+        public int size() {
+          return groups.size();
+        }
+      };
+
   private final Provider<String> canonicalUrl;
   private final Realm realm;
   private final AccountCache accountCache;
@@ -154,7 +179,7 @@
 
   private AccountState state;
   private Set<String> emailAddresses;
-  private Set<AccountGroup.Id> effectiveGroups;
+  private Set<AccountGroup.UUID> effectiveGroups;
   private Set<Change.Id> starredChanges;
   private Collection<AccountProjectWatch> notificationFilters;
 
@@ -217,14 +242,14 @@
   }
 
   @Override
-  public Set<AccountGroup.Id> getEffectiveGroups() {
+  public Set<AccountGroup.UUID> getEffectiveGroups() {
     if (effectiveGroups == null) {
-      Set<AccountGroup.Id> seedGroups;
+      Set<AccountGroup.UUID> seedGroups;
 
       if (authConfig.isIdentityTrustable(state().getExternalIds())) {
         seedGroups = realm.groups(state());
       } else {
-        seedGroups = authConfig.getRegisteredGroups();
+        seedGroups = registeredGroups;
       }
 
       effectiveGroups = getIncludedGroups(seedGroups);
@@ -233,14 +258,14 @@
     return effectiveGroups;
   }
 
-  private Set<AccountGroup.Id> getIncludedGroups(Set<AccountGroup.Id> seedGroups) {
-    Set<AccountGroup.Id> includes = new HashSet<AccountGroup.Id> (seedGroups);
-    Queue<AccountGroup.Id> groupQueue = new LinkedList<AccountGroup.Id> (seedGroups);
+  private Set<AccountGroup.UUID> getIncludedGroups(Set<AccountGroup.UUID> seedGroups) {
+    Set<AccountGroup.UUID> includes = new HashSet<AccountGroup.UUID> (seedGroups);
+    Queue<AccountGroup.UUID> groupQueue = new LinkedList<AccountGroup.UUID> (seedGroups);
 
     while (groupQueue.size() > 0) {
-      AccountGroup.Id id = groupQueue.remove();
+      AccountGroup.UUID id = groupQueue.remove();
 
-      for (final AccountGroup.Id groupId : groupIncludeCache.getByInclude(id)) {
+      for (final AccountGroup.UUID groupId : groupIncludeCache.getByInclude(id)) {
         if (includes.add(groupId)) {
           groupQueue.add(groupId);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
index 26dec09..a4a72a7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
@@ -36,21 +36,21 @@
     PeerDaemonUser create(@Assisted SocketAddress peer);
   }
 
-  private final Set<AccountGroup.Id> effectiveGroups;
+  private final Set<AccountGroup.UUID> effectiveGroups;
   private final SocketAddress peer;
 
   @Inject
   protected PeerDaemonUser(AuthConfig authConfig, @Assisted SocketAddress peer) {
     super(AccessPath.SSH_COMMAND, authConfig);
 
-    final HashSet<AccountGroup.Id> g = new HashSet<AccountGroup.Id>();
+    final HashSet<AccountGroup.UUID> g = new HashSet<AccountGroup.UUID>();
     g.add(authConfig.getAdministratorsGroup());
     this.effectiveGroups = Collections.unmodifiableSet(g);
     this.peer = peer;
   }
 
   @Override
-  public Set<AccountGroup.Id> getEffectiveGroups() {
+  public Set<AccountGroup.UUID> getEffectiveGroups() {
     return effectiveGroups;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
index 5c44bfe..afb8800 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
@@ -28,18 +28,18 @@
 
 public class ReplicationUser extends CurrentUser {
   /** Magic set of groups enabling read of any project and reference. */
-  public static final Set<AccountGroup.Id> EVERYTHING_VISIBLE =
-      Collections.unmodifiableSet(new HashSet<AccountGroup.Id>(0));
+  public static final Set<AccountGroup.UUID> EVERYTHING_VISIBLE =
+      Collections.unmodifiableSet(new HashSet<AccountGroup.UUID>(0));
 
   public interface Factory {
-    ReplicationUser create(@Assisted Set<AccountGroup.Id> authGroups);
+    ReplicationUser create(@Assisted Set<AccountGroup.UUID> authGroups);
   }
 
-  private final Set<AccountGroup.Id> effectiveGroups;
+  private final Set<AccountGroup.UUID> effectiveGroups;
 
   @Inject
   protected ReplicationUser(AuthConfig authConfig,
-      @Assisted Set<AccountGroup.Id> authGroups) {
+      @Assisted Set<AccountGroup.UUID> authGroups) {
     super(AccessPath.REPLICATION, authConfig);
 
     if (authGroups == EVERYTHING_VISIBLE) {
@@ -53,12 +53,12 @@
     }
   }
 
-  private static Set<AccountGroup.Id> copy(Set<AccountGroup.Id> groups) {
-    return Collections.unmodifiableSet(new HashSet<AccountGroup.Id>(groups));
+  private static Set<AccountGroup.UUID> copy(Set<AccountGroup.UUID> groups) {
+    return Collections.unmodifiableSet(new HashSet<AccountGroup.UUID>(groups));
   }
 
   @Override
-  public Set<AccountGroup.Id> getEffectiveGroups() {
+  public Set<AccountGroup.UUID> getEffectiveGroups() {
     return effectiveGroups;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 52ccc66..aea13c7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.EntryCreator;
-import com.google.gerrit.server.config.AuthConfig;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
@@ -90,18 +89,13 @@
 
   static class ByIdLoader extends EntryCreator<Account.Id, AccountState> {
     private final SchemaFactory<ReviewDb> schema;
-    private final Set<AccountGroup.Id> registered;
-    private final Set<AccountGroup.Id> anonymous;
     private final GroupCache groupCache;
     private final Cache<String, Account.Id> byName;
 
     @Inject
-    ByIdLoader(SchemaFactory<ReviewDb> sf, AuthConfig auth,
-        GroupCache groupCache,
+    ByIdLoader(SchemaFactory<ReviewDb> sf, GroupCache groupCache,
         @Named(BYUSER_NAME) Cache<String, Account.Id> byUsername) {
       this.schema = sf;
-      this.registered = auth.getRegisteredGroups();
-      this.anonymous = auth.getAnonymousGroups();
       this.groupCache = groupCache;
       this.byName = byUsername;
     }
@@ -133,21 +127,18 @@
           Collections.unmodifiableCollection(db.accountExternalIds().byAccount(
               who).toList());
 
-      Set<AccountGroup.Id> internalGroups = new HashSet<AccountGroup.Id>();
+      Set<AccountGroup.UUID> internalGroups = new HashSet<AccountGroup.UUID>();
       for (AccountGroupMember g : db.accountGroupMembers().byAccount(who)) {
         final AccountGroup.Id groupId = g.getAccountGroupId();
         final AccountGroup group = groupCache.get(groupId);
         if (group != null && group.getType() == AccountGroup.Type.INTERNAL) {
-          internalGroups.add(groupId);
+          internalGroups.add(group.getGroupUUID());
         }
       }
 
-      if (internalGroups.isEmpty()) {
-        internalGroups = registered;
-      } else {
-        internalGroups.addAll(registered);
-        internalGroups = Collections.unmodifiableSet(internalGroups);
-      }
+      internalGroups.add(AccountGroup.REGISTERED_USERS);
+      internalGroups.add(AccountGroup.ANONYMOUS_USERS);
+      internalGroups = Collections.unmodifiableSet(internalGroups);
 
       return new AccountState(account, internalGroups, externalIds);
     }
@@ -156,6 +147,8 @@
     public AccountState missing(final Account.Id accountId) {
       final Account account = new Account(accountId);
       final Collection<AccountExternalId> ids = Collections.emptySet();
+      final Set<AccountGroup.UUID> anonymous =
+          Collections.singleton(AccountGroup.ANONYMOUS_USERS);
       return new AccountState(account, anonymous, ids);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 5cb8f36..a388eef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -275,7 +275,9 @@
       // is going to be the site's administrator and just make them that
       // to bootstrap the authentication database.
       //
-      final AccountGroup.Id admin = authConfig.getAdministratorsGroup();
+      final AccountGroup.UUID uuid = authConfig.getAdministratorsGroup();
+      final AccountGroup g = db.accountGroups().byUUID(uuid).iterator().next();
+      final AccountGroup.Id admin = g.getId();
       final AccountGroupMember m =
           new AccountGroupMember(new AccountGroupMember.Key(newId, admin));
       db.accountGroupMembersAudit().insert(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
index 9393227..1b036d8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
@@ -26,11 +26,11 @@
 
 public class AccountState {
   private final Account account;
-  private final Set<AccountGroup.Id> internalGroups;
+  private final Set<AccountGroup.UUID> internalGroups;
   private final Collection<AccountExternalId> externalIds;
 
   public AccountState(final Account account,
-      final Set<AccountGroup.Id> actualGroups,
+      final Set<AccountGroup.UUID> actualGroups,
       final Collection<AccountExternalId> externalIds) {
     this.account = account;
     this.internalGroups = actualGroups;
@@ -89,7 +89,7 @@
   }
 
   /** The set of groups maintained directly within the Gerrit database. */
-  public Set<AccountGroup.Id> getInternalGroups() {
+  public Set<AccountGroup.UUID> getInternalGroups() {
     return internalGroups;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index a836f54..77afb13 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -51,7 +51,7 @@
   }
 
   @Override
-  public Set<AccountGroup.Id> groups(final AccountState who) {
+  public Set<AccountGroup.UUID> groups(final AccountState who) {
     return who.getInternalGroups();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
index 978d9c2..6dce197 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
@@ -24,6 +24,8 @@
 
   public AccountGroup get(AccountGroup.NameKey name);
 
+  public AccountGroup get(AccountGroup.UUID uuid);
+
   public Collection<AccountGroup> get(AccountGroup.ExternalNameKey externalName);
 
   public void evict(AccountGroup group);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
index d948aef..4d6dbc1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.EntryCreator;
-import com.google.gerrit.server.config.AuthConfig;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -29,12 +28,14 @@
 import com.google.inject.name.Named;
 
 import java.util.Collection;
+import java.util.List;
 
 /** Tracks group objects in memory for efficient access. */
 @Singleton
 public class GroupCacheImpl implements GroupCache {
   private static final String BYID_NAME = "groups";
   private static final String BYNAME_NAME = "groups_byname";
+  private static final String BYUUID_NAME = "groups_byuuid";
   private static final String BYEXT_NAME = "groups_byext";
 
   public static Module module() {
@@ -49,6 +50,10 @@
             new TypeLiteral<Cache<AccountGroup.NameKey, AccountGroup>>() {};
         core(byName, BYNAME_NAME).populateWith(ByNameLoader.class);
 
+        final TypeLiteral<Cache<AccountGroup.UUID, AccountGroup>> byUUID =
+            new TypeLiteral<Cache<AccountGroup.UUID, AccountGroup>>() {};
+        core(byUUID, BYUUID_NAME).populateWith(ByUUIDLoader.class);
+
         final TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>> byExternalName =
             new TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>>() {};
         core(byExternalName, BYEXT_NAME) //
@@ -62,15 +67,18 @@
 
   private final Cache<AccountGroup.Id, AccountGroup> byId;
   private final Cache<AccountGroup.NameKey, AccountGroup> byName;
+  private final Cache<AccountGroup.UUID, AccountGroup> byUUID;
   private final Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName;
 
   @Inject
   GroupCacheImpl(
       @Named(BYID_NAME) Cache<AccountGroup.Id, AccountGroup> byId,
       @Named(BYNAME_NAME) Cache<AccountGroup.NameKey, AccountGroup> byName,
+      @Named(BYUUID_NAME) Cache<AccountGroup.UUID, AccountGroup> byUUID,
       @Named(BYEXT_NAME) Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName) {
     this.byId = byId;
     this.byName = byName;
+    this.byUUID = byUUID;
     this.byExternalName = byExternalName;
   }
 
@@ -81,6 +89,7 @@
   public void evict(final AccountGroup group) {
     byId.remove(group.getId());
     byName.remove(group.getNameKey());
+    byUUID.remove(group.getGroupUUID());
     byExternalName.remove(group.getExternalNameKey());
   }
 
@@ -92,6 +101,10 @@
     return byName.get(name);
   }
 
+  public AccountGroup get(final AccountGroup.UUID uuid) {
+    return byUUID.get(uuid);
+  }
+
   public Collection<AccountGroup> get(
       final AccountGroup.ExternalNameKey externalName) {
     return byExternalName.get(externalName);
@@ -99,12 +112,10 @@
 
   static class ByIdLoader extends EntryCreator<AccountGroup.Id, AccountGroup> {
     private final SchemaFactory<ReviewDb> schema;
-    private final AccountGroup.Id administrators;
 
     @Inject
-    ByIdLoader(final SchemaFactory<ReviewDb> sf, final AuthConfig authConfig) {
+    ByIdLoader(final SchemaFactory<ReviewDb> sf) {
       schema = sf;
-      administrators = authConfig.getAdministratorsGroup();
     }
 
     @Override
@@ -126,9 +137,8 @@
     public AccountGroup missing(final AccountGroup.Id key) {
       final AccountGroup.NameKey name =
           new AccountGroup.NameKey("Deleted Group" + key.toString());
-      final AccountGroup g = new AccountGroup(name, key);
+      final AccountGroup g = new AccountGroup(name, key, null);
       g.setType(AccountGroup.Type.SYSTEM);
-      g.setOwnerGroupId(administrators);
       return g;
     }
   }
@@ -160,6 +170,32 @@
     }
   }
 
+  static class ByUUIDLoader extends
+      EntryCreator<AccountGroup.UUID, AccountGroup> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    ByUUIDLoader(final SchemaFactory<ReviewDb> sf) {
+      schema = sf;
+    }
+
+    @Override
+    public AccountGroup createEntry(final AccountGroup.UUID uuid)
+        throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        List<AccountGroup> r = db.accountGroups().byUUID(uuid).toList();
+        if (r.size() == 1) {
+          return r.get(0);
+        } else {
+          return null;
+        }
+      } finally {
+        db.close();
+      }
+    }
+  }
+
   static class ByExternalNameLoader extends
       EntryCreator<AccountGroup.ExternalNameKey, Collection<AccountGroup>> {
     private final SchemaFactory<ReviewDb> schema;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index 602b593..722a9e2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -39,11 +39,20 @@
       if (group == null) {
         throw new NoSuchGroupException(groupId);
       }
-      return new GroupControl(user.get(), group);
+      return new GroupControl(groupCache, user.get(), group);
+    }
+
+    public GroupControl controlFor(final AccountGroup.UUID groupId)
+        throws NoSuchGroupException {
+      final AccountGroup group = groupCache.get(groupId);
+      if (group == null) {
+        throw new NoSuchGroupException(groupId);
+      }
+      return new GroupControl(groupCache, user.get(), group);
     }
 
     public GroupControl controlFor(final AccountGroup group) {
-      return new GroupControl(user.get(), group);
+      return new GroupControl(groupCache, user.get(), group);
     }
 
     public GroupControl validateFor(final AccountGroup.Id groupId)
@@ -56,10 +65,13 @@
     }
   }
 
+  private final GroupCache groupCache;
   private final CurrentUser user;
   private final AccountGroup group;
+  private Boolean isOwner;
 
-  GroupControl(final CurrentUser who, final AccountGroup gc) {
+  GroupControl(GroupCache g, CurrentUser who, AccountGroup gc) {
+    groupCache = g;
     user = who;
     group = gc;
   }
@@ -78,9 +90,13 @@
   }
 
   public boolean isOwner() {
-    final AccountGroup.Id owner = group.getOwnerGroupId();
-    return getCurrentUser().getEffectiveGroups().contains(owner)
-        || getCurrentUser().isAdministrator();
+    if (isOwner == null) {
+      AccountGroup g = groupCache.get(group.getOwnerGroupId());
+      AccountGroup.UUID ownerUUID = g != null ? g.getGroupUUID() : null;
+      isOwner = getCurrentUser().getEffectiveGroups().contains(ownerUUID)
+             || getCurrentUser().isAdministrator();
+    }
+    return isOwner;
   }
 
   public boolean canAddMember(final Account.Id id) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
index 3088806..e5f73a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
@@ -20,8 +20,8 @@
 
 /** Tracks group inclusions in memory for efficient access. */
 public interface GroupIncludeCache {
-  public Collection<AccountGroup.Id> getByInclude(AccountGroup.Id groupId);
+  public Collection<AccountGroup.UUID> getByInclude(AccountGroup.UUID groupId);
 
-  public void evictInclude(AccountGroup.Id groupId);
+  public void evictInclude(AccountGroup.UUID groupId);
 }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 76e9231..830d01c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -21,16 +21,17 @@
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gwtorm.client.SchemaFactory;
-
 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 java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 
 /** Tracks group inclusions in memory for efficient access. */
 @Singleton
@@ -41,8 +42,8 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<AccountGroup.Id, Collection<AccountGroup.Id>>> byInclude =
-            new TypeLiteral<Cache<AccountGroup.Id, Collection<AccountGroup.Id>>>() {};
+        final TypeLiteral<Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>>> byInclude =
+            new TypeLiteral<Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>>>() {};
         core(byInclude, BYINCLUDE_NAME).populateWith(ByIncludeLoader.class);
 
         bind(GroupIncludeCacheImpl.class);
@@ -51,23 +52,24 @@
     };
   }
 
-  private final Cache<AccountGroup.Id, Collection<AccountGroup.Id>> byInclude;
+  private final Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>> byInclude;
 
   @Inject
   GroupIncludeCacheImpl(
-      @Named(BYINCLUDE_NAME) Cache<AccountGroup.Id, Collection<AccountGroup.Id>> byInclude) {
+      @Named(BYINCLUDE_NAME) Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>> byInclude) {
     this.byInclude = byInclude;
   }
 
-  public Collection<AccountGroup.Id> getByInclude(final AccountGroup.Id groupId) {
+  public Collection<AccountGroup.UUID> getByInclude(AccountGroup.UUID groupId) {
     return byInclude.get(groupId);
   }
 
-  public void evictInclude(AccountGroup.Id groupId) {
+  public void evictInclude(AccountGroup.UUID groupId) {
     byInclude.remove(groupId);
   }
 
-  static class ByIncludeLoader extends EntryCreator<AccountGroup.Id, Collection<AccountGroup.Id>> {
+  static class ByIncludeLoader extends
+      EntryCreator<AccountGroup.UUID, Collection<AccountGroup.UUID>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -76,14 +78,23 @@
     }
 
     @Override
-    public Collection<AccountGroup.Id> createEntry(final AccountGroup.Id key) throws Exception {
+    public Collection<AccountGroup.UUID> createEntry(final AccountGroup.UUID key) throws Exception {
       final ReviewDb db = schema.open();
       try {
-        ArrayList<AccountGroup.Id> groupArray = new ArrayList<AccountGroup.Id> ();
-        for (AccountGroupInclude agi : db.accountGroupIncludes().byInclude(key)) {
-          groupArray.add(agi.getGroupId());
+        List<AccountGroup> group = db.accountGroups().byUUID(key).toList();
+        if (group.size() != 1) {
+          return Collections.emptyList();
         }
 
+        Set<AccountGroup.Id> ids = new HashSet<AccountGroup.Id>();
+        for (AccountGroupInclude agi : db.accountGroupIncludes().byInclude(group.get(0).getId())) {
+          ids.add(agi.getGroupId());
+        }
+
+        Set<AccountGroup.UUID> groupArray = new HashSet<AccountGroup.UUID> ();
+        for (AccountGroup g : db.accountGroups().get(ids)) {
+          groupArray.add(g.getGroupUUID());
+        }
         return Collections.unmodifiableCollection(groupArray);
       } finally {
         db.close();
@@ -91,7 +102,7 @@
     }
 
     @Override
-    public Collection<AccountGroup.Id> missing(final AccountGroup.Id key) {
+    public Collection<AccountGroup.UUID> missing(final AccountGroup.UUID key) {
       return Collections.emptyList();
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupUUID.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupUUID.java
new file mode 100644
index 0000000..9eec1df
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupUUID.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2010 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.reviewdb.AccountGroup;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+
+import java.security.MessageDigest;
+
+public class GroupUUID {
+  public static AccountGroup.UUID make(String groupName, PersonIdent creator) {
+    MessageDigest md = Constants.newMessageDigest();
+    md.update(Constants.encode("group " + groupName + "\n"));
+    md.update(Constants.encode("creator " + creator.toExternalString() + "\n"));
+    return new AccountGroup.UUID(ObjectId.fromRaw(md.digest()).name());
+  }
+
+  private GroupUUID() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
index aab1cda..a017588 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
@@ -23,14 +23,18 @@
 import com.google.gerrit.reviewdb.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.AccountGroupName;
 import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gwtorm.client.OrmDuplicateKeyException;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.PersonIdent;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 
 public class PerformCreateGroup {
@@ -43,14 +47,18 @@
   private final AccountCache accountCache;
   private final GroupIncludeCache groupIncludeCache;
   private final IdentifiedUser currentUser;
+  private final PersonIdent serverIdent;
 
   @Inject
   PerformCreateGroup(final ReviewDb db, final AccountCache accountCache,
-      final GroupIncludeCache groupIncludeCache, final IdentifiedUser currentUser) {
+      final GroupIncludeCache groupIncludeCache,
+      final IdentifiedUser currentUser,
+      @GerritPersonIdent final PersonIdent serverIdent) {
     this.db = db;
     this.accountCache = accountCache;
     this.groupIncludeCache = groupIncludeCache;
     this.currentUser = currentUser;
+    this.serverIdent = serverIdent;
   }
 
   /**
@@ -81,7 +89,11 @@
     final AccountGroup.Id groupId =
         new AccountGroup.Id(db.nextAccountGroupId());
     final AccountGroup.NameKey nameKey = new AccountGroup.NameKey(groupName);
-    final AccountGroup group = new AccountGroup(nameKey, groupId);
+    final AccountGroup.UUID uuid = GroupUUID.make(groupName,
+        currentUser.newCommitterIdent(
+            serverIdent.getWhen(),
+            serverIdent.getTimeZone()));
+    final AccountGroup group = new AccountGroup(nameKey, groupId, uuid);
     group.setVisibleToAll(visibleToAll);
     if (ownerGroupId != null) {
       group.setOwnerGroupId(ownerGroupId);
@@ -149,8 +161,9 @@
     db.accountGroupIncludes().insert(includeList);
     db.accountGroupIncludesAudit().insert(includesAudit);
 
-    for (AccountGroup.Id includeId : groups) {
-      groupIncludeCache.evictInclude(includeId);
+    for (AccountGroup group : db.accountGroups().get(
+        new HashSet<AccountGroup.Id>(groups))) {
+      groupIncludeCache.evictInclude(group.getGroupUUID());
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
index e42d4ae..072f796 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -27,7 +27,7 @@
 
   public void onCreateAccount(AuthRequest who, Account account);
 
-  public Set<AccountGroup.Id> groups(AccountState who);
+  public Set<AccountGroup.UUID> groups(AccountState who);
 
   /**
    * Locate an account whose local username is the given account name.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
index 675202c..da4f63a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -133,7 +133,7 @@
     }
   }
 
-  Set<AccountGroup.Id> queryForGroups(final DirContext ctx,
+  Set<AccountGroup.UUID> queryForGroups(final DirContext ctx,
       final String username, LdapQuery.Result account)
       throws NamingException, AccountException {
     final LdapSchema schema = getSchema(ctx);
@@ -175,12 +175,12 @@
       }
     }
 
-    final Set<AccountGroup.Id> actual = new HashSet<AccountGroup.Id>();
+    final Set<AccountGroup.UUID> actual = new HashSet<AccountGroup.UUID>();
     for (String dn : groupDNs) {
       for (AccountGroup group : groupCache
           .get(new AccountGroup.ExternalNameKey(dn))) {
         if (group.getType() == AccountGroup.Type.LDAP) {
-          actual.add(group.getId());
+          actual.add(group.getGroupUUID());
         }
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
index 810df28..d41fe82 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
@@ -32,8 +32,8 @@
 
   @Override
   protected void configure() {
-    final TypeLiteral<Cache<String, Set<AccountGroup.Id>>> groups =
-        new TypeLiteral<Cache<String, Set<AccountGroup.Id>>>() {};
+    final TypeLiteral<Cache<String, Set<AccountGroup.UUID>>> groups =
+        new TypeLiteral<Cache<String, Set<AccountGroup.UUID>>>() {};
     core(groups, GROUP_CACHE).maxAge(1, HOURS) //
         .populateWith(LdapRealm.MemberLoader.class);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index de33b44..cd3f2c0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -68,14 +68,14 @@
   private final Cache<String, Account.Id> usernameCache;
   private final Set<Account.FieldName> readOnlyAccountFields;
 
-  private final Cache<String, Set<AccountGroup.Id>> membershipCache;
+  private final Cache<String, Set<AccountGroup.UUID>> membershipCache;
 
   @Inject
   LdapRealm(
       final Helper helper,
       final AuthConfig authConfig,
       final EmailExpander emailExpander,
-      @Named(LdapModule.GROUP_CACHE) final Cache<String, Set<AccountGroup.Id>> membershipCache,
+      @Named(LdapModule.GROUP_CACHE) final Cache<String, Set<AccountGroup.UUID>> membershipCache,
       @Named(LdapModule.USERNAME_CACHE) final Cache<String, Account.Id> usernameCache,
       @GerritServerConfig final Config config) {
     this.helper = helper;
@@ -241,8 +241,8 @@
   }
 
   @Override
-  public Set<AccountGroup.Id> groups(final AccountState who) {
-    final HashSet<AccountGroup.Id> r = new HashSet<AccountGroup.Id>();
+  public Set<AccountGroup.UUID> groups(final AccountState who) {
+    final HashSet<AccountGroup.UUID> r = new HashSet<AccountGroup.UUID>();
     r.addAll(membershipCache.get(findId(who.getExternalIds())));
     r.addAll(who.getInternalGroups());
     return r;
@@ -324,7 +324,7 @@
     }
   }
 
-  static class MemberLoader extends EntryCreator<String, Set<AccountGroup.Id>> {
+  static class MemberLoader extends EntryCreator<String, Set<AccountGroup.UUID>> {
     private final Helper helper;
 
     @Inject
@@ -333,7 +333,7 @@
     }
 
     @Override
-    public Set<AccountGroup.Id> createEntry(final String username)
+    public Set<AccountGroup.UUID> createEntry(final String username)
         throws Exception {
       final DirContext ctx = helper.open();
       try {
@@ -348,7 +348,7 @@
     }
 
     @Override
-    public Set<AccountGroup.Id> missing(final String key) {
+    public Set<AccountGroup.UUID> missing(final String key) {
       return Collections.emptySet();
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ApprovalTypesProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ApprovalTypesProvider.java
index db5bceb..0afaa05 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ApprovalTypesProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ApprovalTypesProvider.java
@@ -39,8 +39,7 @@
 
   @Override
   public ApprovalTypes get() {
-    List<ApprovalType> approvalTypes = new ArrayList<ApprovalType>(2);
-    List<ApprovalType> actionTypes = new ArrayList<ApprovalType>(2);
+    List<ApprovalType> types = new ArrayList<ApprovalType>(2);
 
     try {
       final ReviewDb db = schema.open();
@@ -48,12 +47,7 @@
         for (final ApprovalCategory c : db.approvalCategories().all()) {
           final List<ApprovalCategoryValue> values =
               db.approvalCategoryValues().byCategory(c.getId()).toList();
-          final ApprovalType type = new ApprovalType(c, values);
-          if (type.getCategory().isAction()) {
-            actionTypes.add(type);
-          } else {
-            approvalTypes.add(type);
-          }
+          types.add(new ApprovalType(c, values));
         }
       } finally {
         db.close();
@@ -62,8 +56,6 @@
       throw new ProvisionException("Cannot query approval categories", e);
     }
 
-    approvalTypes = Collections.unmodifiableList(approvalTypes);
-    actionTypes = Collections.unmodifiableList(actionTypes);
-    return new ApprovalTypes(approvalTypes, actionTypes);
+    return new ApprovalTypes(Collections.unmodifiableList(types));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
index 16a6a7c..a747ea4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
@@ -29,9 +29,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
 /** Authentication related settings from {@code gerrit.config}. */
 @Singleton
@@ -45,10 +43,8 @@
   private final boolean cookieSecure;
   private final SignedToken emailReg;
 
-  private final AccountGroup.Id administratorGroup;
-  private final Set<AccountGroup.Id> anonymousGroups;
-  private final Set<AccountGroup.Id> registeredGroups;
-  private final AccountGroup.Id batchUsersGroup;
+  private final AccountGroup.UUID administratorGroup;
+  private final AccountGroup.UUID batchUsersGroup;
 
   private final boolean allowGoogleAccountUpgrade;
 
@@ -64,13 +60,8 @@
     cookieSecure = cfg.getBoolean("auth", "cookiesecure", false);
     emailReg = new SignedToken(5 * 24 * 60 * 60, s.registerEmailPrivateKey);
 
-    final HashSet<AccountGroup.Id> r = new HashSet<AccountGroup.Id>(2);
-    r.add(s.anonymousGroupId);
-    r.add(s.registeredGroupId);
-    registeredGroups = Collections.unmodifiableSet(r);
-    anonymousGroups = Collections.singleton(s.anonymousGroupId);
-    administratorGroup = s.adminGroupId;
-    batchUsersGroup = s.batchUsersGroupId;
+    administratorGroup = s.adminGroupUUID;
+    batchUsersGroup = s.batchUsersGroupUUID;
 
     if (authType == AuthType.OPENID) {
       allowGoogleAccountUpgrade =
@@ -127,25 +118,15 @@
   }
 
   /** Identity of the magic group with full powers. */
-  public AccountGroup.Id getAdministratorsGroup() {
+  public AccountGroup.UUID getAdministratorsGroup() {
     return administratorGroup;
   }
 
   /** Identity of the group whose service is degraded to lower priority. */
-  public AccountGroup.Id getBatchUsersGroup() {
+  public AccountGroup.UUID getBatchUsersGroup() {
     return batchUsersGroup;
   }
 
-  /** Groups that all users, including anonymous users, belong to. */
-  public Set<AccountGroup.Id> getAnonymousGroups() {
-    return anonymousGroups;
-  }
-
-  /** Groups that all users who have created an account belong to. */
-  public Set<AccountGroup.Id> getRegisteredGroups() {
-    return registeredGroups;
-  }
-
   /** OpenID identities which the server permits for authentication. */
   public List<OpenIdProviderPattern> getAllowedOpenIDs() {
     return allowedOpenIDs;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
index f24dcc5..3c102ba 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -310,10 +310,10 @@
    * @return the actual groups resolved from the database. If no groups are
    *         found, returns an empty {@code Set}, never {@code null}.
    */
-  public static Set<AccountGroup.Id> groupsFor(
+  public static Set<AccountGroup.UUID> groupsFor(
       SchemaFactory<ReviewDb> dbfactory, String[] groupNames, Logger log,
       String groupNotFoundWarning) {
-    final Set<AccountGroup.Id> result = new HashSet<AccountGroup.Id>();
+    final Set<AccountGroup.UUID> result = new HashSet<AccountGroup.UUID>();
     try {
       final ReviewDb db = dbfactory.open();
       try {
@@ -322,9 +322,16 @@
               db.accountGroupNames().get(new AccountGroup.NameKey(name));
           if (group == null) {
             log.warn(MessageFormat.format(groupNotFoundWarning, name));
-          } else {
-            result.add(group.getId());
+            continue;
           }
+
+          AccountGroup ag = db.accountGroups().get(group.getId());
+          if (ag == null) {
+            log.warn(MessageFormat.format(groupNotFoundWarning, name));
+            continue;
+          }
+
+          result.add(ag.getGroupUUID());
         }
       } finally {
         db.close();
@@ -345,7 +352,7 @@
    * @return the actual groups resolved from the database. If no groups are
    *         found, returns an empty {@code Set}, never {@code null}.
    */
-  public static Set<AccountGroup.Id> groupsFor(
+  public static Set<AccountGroup.UUID> groupsFor(
       SchemaFactory<ReviewDb> dbfactory, String[] groupNames, Logger log) {
     return groupsFor(dbfactory, groupNames, log,
         "Group \"{0}\" not in database, skipping.");
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 5426f34..3f400a6 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
@@ -23,8 +23,6 @@
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.FileTypeRegistry;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.MimeUtilFileTypeRegistry;
 import com.google.gerrit.server.ReplicationUser;
@@ -41,8 +39,7 @@
 import com.google.gerrit.server.cache.CachePool;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.git.ChangeMergeQueue;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.git.GitModule;
 import com.google.gerrit.server.git.MergeQueue;
 import com.google.gerrit.server.git.PushAllProjectsOp;
 import com.google.gerrit.server.git.PushReplication;
@@ -71,7 +68,6 @@
 import org.apache.velocity.app.Velocity;
 import org.apache.velocity.runtime.RuntimeConstants;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.PersonIdent;
 
 import java.util.Properties;
 
@@ -143,9 +139,6 @@
         SINGLETON);
     bind(AnonymousUser.class);
 
-    bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class).toProvider(
-        GerritPersonIdentProvider.class);
-
     bind(IdGenerator.class);
     bind(CachePool.class);
     install(AccountByEmailCacheImpl.module());
@@ -155,13 +148,13 @@
     install(PatchListCacheImpl.module());
     install(ProjectCacheImpl.module());
     install(new AccessControlModule());
+    install(new GitModule());
 
     factory(AccountInfoCacheFactory.Factory.class);
     factory(GroupInfoCacheFactory.Factory.class);
     factory(ProjectState.Factory.class);
     factory(RefControl.Factory.class);
 
-    bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class);
     bind(FileTypeRegistry.class).to(MimeUtilFileTypeRegistry.class);
     bind(WorkQueue.class);
     bind(ToolsCatalog.class);
@@ -189,7 +182,6 @@
     install(new LifecycleModule() {
       @Override
       protected void configure() {
-        listener().to(LocalDiskRepositoryManager.Lifecycle.class);
         listener().to(CachePool.Lifecycle.class);
         listener().to(WorkQueue.Lifecycle.class);
         listener().to(VelocityLifecycle.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 f6dad7d..52525e7 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
@@ -24,6 +24,7 @@
 import com.google.gerrit.server.account.PerformCreateGroup;
 import com.google.gerrit.server.git.CreateCodeReviewNotes;
 import com.google.gerrit.server.git.MergeOp;
+import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ReceiveCommits;
 import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.AddReviewerSender;
@@ -49,6 +50,7 @@
     bind(ReviewDb.class).toProvider(RequestScopedReviewDbProvider.class).in(
         RequestScoped.class);
     bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
+    bind(MetaDataUpdate.User.class).in(RequestScoped.class);
     bind(AccountResolver.class);
     bind(ChangeQueryRewriter.class);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
index 9af6d62..1c13b9e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
@@ -22,21 +22,17 @@
 import org.eclipse.jgit.lib.Config;
 
 import java.util.Collections;
-import java.util.HashSet;
 
 public class GitReceivePackGroupsProvider extends GroupSetProvider {
   @Inject
   public GitReceivePackGroupsProvider(@GerritServerConfig Config config,
-      AuthConfig authConfig, SchemaFactory<ReviewDb> db) {
+      SchemaFactory<ReviewDb> db) {
     super(config, db, "receive", null, "allowGroup");
 
     // If no group was set, default to "registered users"
     //
     if (groupIds.isEmpty()) {
-      HashSet<AccountGroup.Id> all = new HashSet<AccountGroup.Id>();
-      all.addAll(authConfig.getRegisteredGroups());
-      all.removeAll(authConfig.getAnonymousGroups());
-      groupIds = Collections.unmodifiableSet(all);
+      groupIds = Collections.singleton(AccountGroup.REGISTERED_USERS);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
index bfb09a5..65e6900 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
@@ -27,15 +27,15 @@
 public class GitUploadPackGroupsProvider extends GroupSetProvider {
   @Inject
   public GitUploadPackGroupsProvider(@GerritServerConfig Config config,
-      AuthConfig authConfig, SchemaFactory<ReviewDb> db) {
+      SchemaFactory<ReviewDb> db) {
     super(config, db, "upload", null, "allowGroup");
 
     // If no group was set, default to "registered users" and "anonymous"
     //
     if (groupIds.isEmpty()) {
-      HashSet<AccountGroup.Id> all = new HashSet<AccountGroup.Id>();
-      all.addAll(authConfig.getRegisteredGroups());
-      all.addAll(authConfig.getAnonymousGroups());
+      HashSet<AccountGroup.UUID> all = new HashSet<AccountGroup.UUID>();
+      all.add(AccountGroup.REGISTERED_USERS);
+      all.add(AccountGroup.ANONYMOUS_USERS);
       groupIds = Collections.unmodifiableSet(all);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
index 373fdb5..6ff977f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -30,11 +30,11 @@
 import java.util.Set;
 
 public abstract class GroupSetProvider implements
-    Provider<Set<AccountGroup.Id>> {
+    Provider<Set<AccountGroup.UUID>> {
   private static final Logger log =
       LoggerFactory.getLogger(GroupSetProvider.class);
 
-  protected Set<AccountGroup.Id> groupIds;
+  protected Set<AccountGroup.UUID> groupIds;
 
   @Inject
   protected GroupSetProvider(@GerritServerConfig Config config,
@@ -44,7 +44,7 @@
   }
 
   @Override
-  public Set<AccountGroup.Id> get() {
+  public Set<AccountGroup.UUID> get() {
     return groupIds;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroupsProvider.java
index 381c914..e58e8bf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroupsProvider.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.config;
 
 import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.SystemConfig;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
 
@@ -37,11 +36,11 @@
 public class ProjectCreatorGroupsProvider extends GroupSetProvider {
   @Inject
   public ProjectCreatorGroupsProvider(@GerritServerConfig final Config config,
-      final SystemConfig systemConfig, final SchemaFactory<ReviewDb> db) {
+      final AuthConfig authConfig, final SchemaFactory<ReviewDb> db) {
     super(config, db, "repository", "*", "createGroup");
 
     if (groupIds.isEmpty()) {
-      groupIds = Collections.singleton(systemConfig.adminGroupId);
+      groupIds = Collections.singleton(authConfig.getAdministratorsGroup());
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
index c457d73..4d5ad0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
@@ -37,7 +37,7 @@
 public class ProjectOwnerGroupsProvider extends GroupSetProvider {
   @Inject
   public ProjectOwnerGroupsProvider(
-      @ProjectCreatorGroups final Set<AccountGroup.Id> creatorGroups,
+      @ProjectCreatorGroups final Set<AccountGroup.UUID> creatorGroups,
       @GerritServerConfig final Config config, final SchemaFactory<ReviewDb> db) {
     super(config, db, "repository", "*", "ownerGroup");
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePathFromSystemConfigProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePathFromSystemConfigProvider.java
deleted file mode 100644
index 5b5dcc1..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePathFromSystemConfigProvider.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2009 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.config;
-
-import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.io.File;
-
-/** Provides {@link java.io.File} annotated with {@link SitePath}. */
-public class SitePathFromSystemConfigProvider implements Provider<File> {
-  private final File path;
-
-  @Inject
-  SitePathFromSystemConfigProvider(final SystemConfig config) {
-    final String p = config.sitePath;
-    path = new File(p != null && p.length() > 0 ? p : ".").getAbsoluteFile();
-  }
-
-  @Override
-  public File get() {
-    return path;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
index 573e6bb..56ac0c1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -27,7 +27,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import com.google.inject.internal.Nullable;
 
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -35,6 +34,8 @@
 import java.util.Collection;
 import java.util.Map;
 
+import javax.annotation.Nullable;
+
 @Singleton
 public class EventFactory {
   private final AccountCache accountCache;
@@ -212,7 +213,7 @@
     a.by = asAccountAttribute(approval.getAccountId());
     a.grantedOn = approval.getGranted().getTime() / 1000L;
 
-    ApprovalType at = approvalTypes.getApprovalType(approval.getCategoryId());
+    ApprovalType at = approvalTypes.byId(approval.getCategoryId());
     if (at != null) {
       a.description = at.getCategory().getName();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java
index 976ec2e..fb88ad6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java
@@ -16,6 +16,7 @@
 
 import static com.google.gerrit.server.git.GitRepositoryManager.REFS_NOTES_REVIEW;
 
+import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.Change;
@@ -197,10 +198,13 @@
         } else if (ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
           submit = a;
         } else {
-          formatter.appendApproval(
-              approvalTypes.getApprovalType(a.getCategoryId()).getCategory(),
-              a.getValue(),
-              accountCache.get(a.getAccountId()).getAccount());
+          ApprovalType type = approvalTypes.byId(a.getCategoryId());
+          if (type != null) {
+            formatter.appendApproval(
+                type.getCategory(),
+                a.getValue(),
+                accountCache.get(a.getAccountId()).getAccount());
+          }
         }
       }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
similarity index 63%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
index 0977ee9..7947d30 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
@@ -12,15 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.server.git;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.gerrit.server.config.FactoryModule;
 
-public class Schema_49 extends SchemaVersion {
-
-  @Inject
-  Schema_49(Provider<Schema_48> prior) {
-    super(prior);
+/** Configures the Git support. */
+public class GitModule extends FactoryModule {
+  @Override
+  protected void configure() {
+    factory(RenameGroupOp.Factory.class);
+    factory(MetaDataUpdate.InternalFactory.class);
+    bind(MetaDataUpdate.Server.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java
deleted file mode 100644
index 3f2ff0d..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright (C) 2009 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;
-
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.Project.SubmitType;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
-import com.google.inject.Inject;
-
-import org.eclipse.jgit.lib.RepositoryCache.FileKey;
-import org.eclipse.jgit.util.FS;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
-/** Imports all projects found within the repository manager. */
-public class GitProjectImporter {
-  public interface Messages {
-    void info(String msg);
-    void warning(String msg);
-  }
-
-  private final LocalDiskRepositoryManager repositoryManager;
-  private final SchemaFactory<ReviewDb> schema;
-  private Messages messages;
-
-  @Inject
-  GitProjectImporter(final LocalDiskRepositoryManager repositoryManager,
-      final SchemaFactory<ReviewDb> schema) {
-    this.repositoryManager = repositoryManager;
-    this.schema = schema;
-  }
-
-  public void run(final Messages msg) throws OrmException, IOException {
-    messages = msg;
-    messages.info("Scanning " + repositoryManager.getBasePath());
-    final ReviewDb db = schema.open();
-    try {
-      final HashSet<String> have = new HashSet<String>();
-      for (Project p : db.projects().all()) {
-        have.add(p.getName());
-      }
-      importProjects(repositoryManager.getBasePath(), "", db, have);
-    } finally {
-      db.close();
-    }
-  }
-
-  private void importProjects(final File dir, final String prefix,
-      final ReviewDb db, final Set<String> have) throws OrmException,
-      IOException {
-    final File[] ls = dir.listFiles();
-    if (ls == null) {
-      return;
-    }
-
-    for (File f : ls) {
-      String name = f.getName();
-      if (".".equals(name) || "..".equals(name)) {
-        continue;
-      }
-
-      if (FileKey.isGitRepository(f, FS.DETECTED)) {
-        if (name.equals(".git")) {
-          if ("".equals(prefix)) {
-            // If the git base path is itself a git repository working
-            // directory, this is a bit nonsensical for Gerrit Code Review.
-            // Skip the path and do the next one.
-            messages.warning("Skipping " + f.getAbsolutePath());
-            continue;
-          }
-          name = prefix.substring(0, prefix.length() - 1);
-
-        } else if (name.endsWith(".git")) {
-          name = prefix + name.substring(0, name.length() - 4);
-
-        } else {
-          name = prefix + name;
-          if (!have.contains(name)) {
-            messages.warning("Importing non-standard name '" + name + "'");
-          }
-        }
-
-        if (have.contains(name)) {
-          continue;
-        }
-
-        final Project.NameKey nameKey = new Project.NameKey(name);
-        final Project p = new Project(nameKey);
-
-        p.setDescription(repositoryManager.getProjectDescription(nameKey));
-        p.setSubmitType(SubmitType.MERGE_IF_NECESSARY);
-        p.setUseContributorAgreements(false);
-        p.setUseSignedOffBy(false);
-        p.setUseContentMerge(false);
-        p.setRequireChangeID(false);
-        db.projects().insert(Collections.singleton(p));
-
-      } else if (f.isDirectory()) {
-        importProjects(f, prefix + f.getName() + "/", db, have);
-      }
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
index a19b722..3e3d929 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
@@ -21,7 +21,7 @@
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
-
+import java.util.SortedSet;
 
 /**
  * Manages Git repositories for the Gerrit server process.
@@ -37,6 +37,9 @@
   /** Note tree listing commits we refuse {@code refs/meta/reject-commits} */
   public static final String REF_REJECT_COMMITS = "refs/meta/reject-commits";
 
+  /** Configuration settings for a project {@code refs/meta/config} */
+  public static final String REF_CONFIG = "refs/meta/config";
+
   /**
    * Get (or open) a repository by name.
    *
@@ -61,6 +64,9 @@
   public abstract Repository createRepository(Project.NameKey name)
       throws RepositoryNotFoundException;
 
+  /** @return set of all known projects, sorted by natural NameKey order. */
+  public abstract SortedSet<Project.NameKey> list();
+
   /**
    * Read the {@code GIT_DIR/description} file for gitweb.
    * <p>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 7e98348..1d348f7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -23,10 +23,12 @@
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryCache;
 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.storage.file.LockFile;
 import org.eclipse.jgit.storage.file.WindowCache;
 import org.eclipse.jgit.storage.file.WindowCacheConfig;
@@ -39,6 +41,9 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.util.Collections;
+import java.util.SortedSet;
+import java.util.TreeSet;
 
 /** Manages Git repositories stored on the local filesystem. */
 @Singleton
@@ -129,10 +134,19 @@
         }
         loc = FileKey.exact(new File(basePath, n), FS.DETECTED);
       }
-      return RepositoryCache.open(loc, false);
+
+      Repository db = RepositoryCache.open(loc, false);
+      db.create(true /* bare */);
+
+      StoredConfig config = db.getConfig();
+      config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION,
+        null, ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true);
+      config.save();
+
+      return db;
     } catch (IOException e1) {
       final RepositoryNotFoundException e2;
-      e2 = new RepositoryNotFoundException("Cannot open repository " + name);
+      e2 = new RepositoryNotFoundException("Cannot create repository " + name);
       e2.initCause(e1);
       throw e2;
     }
@@ -142,41 +156,48 @@
       throws RepositoryNotFoundException, IOException {
     final Repository e = openRepository(name);
     try {
-      final File d = new File(e.getDirectory(), "description");
-
-      String description;
-      try {
-        description = RawParseUtils.decode(IO.readFully(d));
-      } catch (FileNotFoundException err) {
-        return null;
-      }
-
-      if (description != null) {
-        description = description.trim();
-        if (description.isEmpty()) {
-          description = null;
-        }
-        if (UNNAMED.equals(description)) {
-          description = null;
-        }
-      }
-      return description;
+      return getProjectDescription(e);
     } finally {
       e.close();
     }
   }
 
+  private String getProjectDescription(final Repository e) throws IOException {
+    final File d = new File(e.getDirectory(), "description");
+
+    String description;
+    try {
+      description = RawParseUtils.decode(IO.readFully(d));
+    } catch (FileNotFoundException err) {
+      return null;
+    }
+
+    if (description != null) {
+      description = description.trim();
+      if (description.isEmpty()) {
+        description = null;
+      }
+      if (UNNAMED.equals(description)) {
+        description = null;
+      }
+    }
+    return description;
+  }
+
   public void setProjectDescription(final Project.NameKey name,
       final String description) {
     // Update git's description file, in case gitweb is being used
     //
     try {
-      final Repository e;
-      final LockFile f;
-
-      e = openRepository(name);
+      final Repository e = openRepository(name);
       try {
-        f = new LockFile(new File(e.getDirectory(), "description"), FS.DETECTED);
+        final String old = getProjectDescription(e);
+        if ((old == null && description == null)
+            || (old != null && old.equals(description))) {
+          return;
+        }
+
+        final LockFile f = new LockFile(new File(e.getDirectory(), "description"), FS.DETECTED);
         if (f.lock()) {
           String d = description;
           if (d != null) {
@@ -216,4 +237,46 @@
 
     return false; // is a reasonable name
   }
+
+  @Override
+  public SortedSet<Project.NameKey> list() {
+    SortedSet<Project.NameKey> names = new TreeSet<Project.NameKey>();
+    scanProjects(basePath, "", names);
+    return Collections.unmodifiableSortedSet(names);
+  }
+
+  private void scanProjects(final File dir, final String prefix,
+      final SortedSet<Project.NameKey> names) {
+    final File[] ls = dir.listFiles();
+    if (ls == null) {
+      return;
+    }
+
+    for (File f : ls) {
+      String fileName = f.getName();
+      if (FileKey.isGitRepository(f, FS.DETECTED)) {
+        String projectName;
+        if (fileName.equals(Constants.DOT_GIT)) {
+          projectName = prefix.substring(0, prefix.length() - 1);
+
+        } else if (fileName.endsWith(Constants.DOT_GIT_EXT)) {
+          int newLen = fileName.length() - Constants.DOT_GIT_EXT.length();
+          projectName = prefix + fileName.substring(0, newLen);
+
+        } else {
+          projectName = prefix + fileName;
+        }
+
+        Project.NameKey nameKey = new Project.NameKey(projectName);
+        if (isUnreasonableName(nameKey)) {
+          log.warn("Ignoring unreasonably named repository " + f.getAbsolutePath());
+        } else {
+          names.add(nameKey);
+        }
+
+      } else if (f.isDirectory()) {
+        scanProjects(f, prefix + f.getName() + "/", names);
+      }
+    }
+  }
 }
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 b66fcb59..c69119b 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
@@ -805,7 +805,7 @@
           tag = "Tested-by";
         } else {
           final ApprovalType at =
-              approvalTypes.getApprovalType(a.getCategoryId());
+              approvalTypes.byId(a.getCategoryId());
           if (at == null) {
             // A deprecated/deleted approval type, ignore it.
             continue;
@@ -882,6 +882,17 @@
 
   private void updateBranch() throws MergeException {
     if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) {
+      if (GitRepositoryManager.REF_CONFIG.equals(branchUpdate.getName())) {
+        try {
+          ProjectConfig cfg = new ProjectConfig(destProject.getNameKey());
+          cfg.load(db, mergeTip);
+        } catch (Exception e) {
+          throw new MergeException("Submit would store invalid"
+              + " project configuration " + mergeTip.name() + " for "
+              + destProject.getName(), e);
+        }
+      }
+
       branchUpdate.setForceUpdate(false);
       branchUpdate.setNewObjectId(mergeTip);
       branchUpdate.setRefLogMessage("merged", true);
@@ -889,6 +900,13 @@
         switch (branchUpdate.update(rw)) {
           case NEW:
           case FAST_FORWARD:
+            if (GitRepositoryManager.REF_CONFIG.equals(branchUpdate.getName())) {
+              projectCache.evict(destProject);
+              ProjectState ps = projectCache.get(destProject.getNameKey());
+              repoManager.setProjectDescription(destProject.getNameKey(), //
+                  ps.getProject().getDescription());
+            }
+
             replication.scheduleUpdate(destBranch.getParentKey(), branchUpdate
                 .getName());
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
new file mode 100644
index 0000000..568c8cf9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
@@ -0,0 +1,128 @@
+// 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;
+
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+/** Helps with the updating of a {@link VersionedMetaData}. */
+public class MetaDataUpdate {
+  public static class User {
+    private final InternalFactory factory;
+    private final GitRepositoryManager mgr;
+    private final PersonIdent serverIdent;
+    private final PersonIdent userIdent;
+
+    @Inject
+    User(InternalFactory factory, GitRepositoryManager mgr,
+        @GerritPersonIdent PersonIdent serverIdent, IdentifiedUser currentUser) {
+      this.factory = factory;
+      this.mgr = mgr;
+      this.serverIdent = serverIdent;
+      this.userIdent = currentUser.newCommitterIdent( //
+          serverIdent.getWhen(), //
+          serverIdent.getTimeZone());
+    }
+
+    public PersonIdent getUserPersonIdent() {
+      return userIdent;
+    }
+
+    public MetaDataUpdate create(Project.NameKey name)
+        throws RepositoryNotFoundException {
+      MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
+      md.getCommitBuilder().setAuthor(userIdent);
+      md.getCommitBuilder().setCommitter(serverIdent);
+      return md;
+    }
+  }
+
+  public static class Server {
+    private final InternalFactory factory;
+    private final GitRepositoryManager mgr;
+    private final PersonIdent serverIdent;
+
+    @Inject
+    Server(InternalFactory factory, GitRepositoryManager mgr,
+        @GerritPersonIdent PersonIdent serverIdent) {
+      this.factory = factory;
+      this.mgr = mgr;
+      this.serverIdent = serverIdent;
+    }
+
+    public MetaDataUpdate create(Project.NameKey name)
+        throws RepositoryNotFoundException {
+      MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
+      md.getCommitBuilder().setAuthor(serverIdent);
+      md.getCommitBuilder().setCommitter(serverIdent);
+      return md;
+    }
+  }
+
+  interface InternalFactory {
+    MetaDataUpdate create(@Assisted Project.NameKey projectName,
+        @Assisted Repository db);
+  }
+
+  private final ReplicationQueue replication;
+  private final Project.NameKey projectName;
+  private final Repository db;
+  private final CommitBuilder commit;
+
+  @Inject
+  public MetaDataUpdate(ReplicationQueue replication,
+      @Assisted Project.NameKey projectName, @Assisted Repository db) {
+    this.replication = replication;
+    this.projectName = projectName;
+    this.db = db;
+    this.commit = new CommitBuilder();
+  }
+
+  /** Set the commit message used when committing the update. */
+  public void setMessage(String message) {
+    getCommitBuilder().setMessage(message);
+  }
+
+  /** Close the cached Repository handle. */
+  public void close() {
+    getRepository().close();
+  }
+
+  Project.NameKey getProjectName() {
+    return projectName;
+  }
+
+  Repository getRepository() {
+    return db;
+  }
+
+  public CommitBuilder getCommitBuilder() {
+    return commit;
+  }
+
+  void replicate(String ref) {
+    if (replication.isEnabled()) {
+      replication.scheduleUpdate(projectName, ref);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java
new file mode 100644
index 0000000..ceb70d2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java
@@ -0,0 +1,37 @@
+// 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;
+
+import com.google.gerrit.reviewdb.Project;
+
+/** A disabled {@link ReplicationQueue}. */
+public final class NoReplication implements ReplicationQueue {
+  @Override
+  public boolean isEnabled() {
+    return false;
+  }
+
+  @Override
+  public void scheduleUpdate(Project.NameKey project, String ref) {
+  }
+
+  @Override
+  public void scheduleFullSync(Project.NameKey project, String urlMatch) {
+  }
+
+  @Override
+  public void replicateNewProject(Project.NameKey project, String head) {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
new file mode 100644
index 0000000..651fc6a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -0,0 +1,391 @@
+// Copyright (C) 2010 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;
+
+import static com.google.gerrit.common.data.AccessSection.isAccessSection;
+import static com.google.gerrit.common.data.Permission.isPermission;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.Project.SubmitType;
+import com.google.gerrit.server.account.GroupCache;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+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;
+
+public class ProjectConfig extends VersionedMetaData {
+  private static final String PROJECT_CONFIG = "project.config";
+  private static final String GROUP_LIST = "groups";
+
+  private static final String PROJECT = "project";
+  private static final String KEY_DESCRIPTION = "description";
+
+  private static final String ACCESS = "access";
+  private static final String KEY_INHERIT_FROM = "inheritFrom";
+  private static final String KEY_GROUP_PERMISSIONS = "exclusiveGroupPermissions";
+
+  private static final String RECEIVE = "receive";
+  private static final String KEY_REQUIRE_SIGNED_OFF_BY = "requireSignedOffBy";
+  private static final String KEY_REQUIRE_CHANGE_ID = "requireChangeId";
+  private static final String KEY_REQUIRE_CONTRIBUTOR_AGREEMENT =
+      "requireContributorAgreement";
+
+  private static final String SUBMIT = "submit";
+  private static final String KEY_ACTION = "action";
+  private static final String KEY_MERGE_CONTENT = "mergeContent";
+
+  private static final SubmitType defaultSubmitAction =
+      SubmitType.MERGE_IF_NECESSARY;
+
+  private Project.NameKey projectName;
+  private Project project;
+  private Map<AccountGroup.UUID, GroupReference> groupsByUUID;
+  private Map<String, AccessSection> accessSections;
+
+  public static ProjectConfig read(MetaDataUpdate update) throws IOException,
+      ConfigInvalidException {
+    ProjectConfig r = new ProjectConfig(update.getProjectName());
+    r.load(update);
+    return r;
+  }
+
+  public static ProjectConfig read(MetaDataUpdate update, ObjectId id)
+      throws IOException, ConfigInvalidException {
+    ProjectConfig r = new ProjectConfig(update.getProjectName());
+    r.load(update, id);
+    return r;
+  }
+
+  public ProjectConfig(Project.NameKey projectName) {
+    this.projectName = projectName;
+  }
+
+  public Project getProject() {
+    return project;
+  }
+
+  public AccessSection getAccessSection(String name) {
+    return getAccessSection(name, false);
+  }
+
+  public AccessSection getAccessSection(String name, boolean create) {
+    AccessSection as = accessSections.get(name);
+    if (as == null && create) {
+      as = new AccessSection(name);
+      accessSections.put(name, as);
+    }
+    return as;
+  }
+
+  public Collection<AccessSection> getAccessSections() {
+    return sort(accessSections.values());
+  }
+
+  public void remove(AccessSection section) {
+    if (section != null) {
+      accessSections.remove(section.getRefPattern());
+    }
+  }
+
+  public void replace(AccessSection section) {
+    for (Permission permission : section.getPermissions()) {
+      for (PermissionRule rule : permission.getRules()) {
+        rule.setGroup(resolve(rule.getGroup()));
+      }
+    }
+
+    accessSections.put(section.getRefPattern(), section);
+  }
+
+  public GroupReference resolve(AccountGroup group) {
+    return resolve(GroupReference.forGroup(group));
+  }
+
+  public GroupReference resolve(GroupReference group) {
+    if (group != null) {
+      GroupReference ref = groupsByUUID.get(group.getUUID());
+      if (ref != null) {
+        return ref;
+      }
+      groupsByUUID.put(group.getUUID(), group);
+    }
+    return group;
+  }
+
+  /** @return the group reference, if the group is used by at least one rule. */
+  public GroupReference getGroup(AccountGroup.UUID uuid) {
+    return groupsByUUID.get(uuid);
+  }
+
+  /**
+   * Check all GroupReferences use current group name, repairing stale ones.
+   *
+   * @param groupCache cache to use when looking up group information by UUID.
+   * @return true if one or more group names was stale.
+   */
+  public boolean updateGroupNames(GroupCache groupCache) {
+    boolean dirty = false;
+    for (GroupReference ref : groupsByUUID.values()) {
+      AccountGroup g = groupCache.get(ref.getUUID());
+      if (g != null && !g.getName().equals(ref.getName())) {
+        dirty = true;
+        ref.setName(g.getName());
+      }
+    }
+    return dirty;
+  }
+
+  @Override
+  protected String getRefName() {
+    return GitRepositoryManager.REF_CONFIG;
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    Map<String,GroupReference> groupsByName = readGroupList();
+
+    Config rc = readConfig(PROJECT_CONFIG);
+    project = new Project(projectName);
+
+    Project p = project;
+    p.setDescription(rc.getString(PROJECT, null, KEY_DESCRIPTION));
+    if (p.getDescription() == null) {
+      p.setDescription("");
+    }
+    p.setParentName(rc.getString(ACCESS, null, KEY_INHERIT_FROM));
+
+    p.setUseContributorAgreements(rc.getBoolean(RECEIVE, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, false));
+    p.setUseSignedOffBy(rc.getBoolean(RECEIVE, KEY_REQUIRE_SIGNED_OFF_BY, false));
+    p.setRequireChangeID(rc.getBoolean(RECEIVE, KEY_REQUIRE_CHANGE_ID, false));
+
+    p.setSubmitType(rc.getEnum(SUBMIT, null, KEY_ACTION, defaultSubmitAction));
+    p.setUseContentMerge(rc.getBoolean(SUBMIT, null, KEY_MERGE_CONTENT, false));
+
+    accessSections = new HashMap<String, AccessSection>();
+    for (String refName : rc.getSubsections(ACCESS)) {
+      if (isAccessSection(refName)) {
+        AccessSection as = getAccessSection(refName, true);
+
+        for (String varName : rc.getStringList(ACCESS, refName, KEY_GROUP_PERMISSIONS)) {
+          for (String n : varName.split("[, \t]{1,}")) {
+            if (isPermission(n)) {
+              as.getPermission(n, true).setExclusiveGroup(true);
+            }
+          }
+        }
+
+        for (String varName : rc.getNames(ACCESS, refName)) {
+          if (isPermission(varName)) {
+            Permission perm = as.getPermission(varName, true);
+
+            boolean useRange = perm.isLabel();
+            for (String ruleString : rc.getStringList(ACCESS, refName, varName)) {
+              PermissionRule rule;
+              try {
+                rule = PermissionRule.fromString(ruleString, useRange);
+              } catch (IllegalArgumentException notRule) {
+                throw new ConfigInvalidException("Invalid rule in " + ACCESS
+                    + "." + refName + "." + varName + ": "
+                    + notRule.getMessage(), notRule);
+              }
+
+              GroupReference ref = groupsByName.get(rule.getGroup().getName());
+              if (ref == null) {
+                // The group wasn't mentioned in the groups table, so there is
+                // no valid UUID for it. Pool the reference anyway so at least
+                // all rules in the same file share the same GroupReference.
+                //
+                ref = rule.getGroup();
+                groupsByName.put(ref.getName(), ref);
+              }
+
+              rule.setGroup(ref);
+              perm.add(rule);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  private Map<String, GroupReference> readGroupList() throws IOException,
+      ConfigInvalidException {
+    groupsByUUID = new HashMap<AccountGroup.UUID, GroupReference>();
+    Map<String, GroupReference> groupsByName =
+        new HashMap<String, GroupReference>();
+
+    BufferedReader br = new BufferedReader(new StringReader(readUTF8(GROUP_LIST)));
+    String s;
+    while ((s = br.readLine()) != null) {
+      if (s.isEmpty() || s.startsWith("#")) {
+        continue;
+      }
+
+      int tab = s.indexOf('\t');
+      if (tab < 0) {
+        throw new ConfigInvalidException("Invalid group line: " + s);
+      }
+
+      AccountGroup.UUID uuid = new AccountGroup.UUID(s.substring(0, tab).trim());
+      String name = s.substring(tab + 1).trim();
+      GroupReference ref = new GroupReference(uuid, name);
+
+      groupsByUUID.put(uuid, ref);
+      groupsByName.put(name, ref);
+    }
+    return groupsByName;
+  }
+
+  @Override
+  protected void onSave(CommitBuilder commit) throws IOException,
+      ConfigInvalidException {
+    if (commit.getMessage() == null || "".equals(commit.getMessage())) {
+      commit.setMessage("Updated project configuration\n");
+    }
+
+    Config rc = readConfig(PROJECT_CONFIG);
+    Project p = project;
+
+    if (p.getDescription() != null && !p.getDescription().isEmpty()) {
+      rc.setString(PROJECT, null, KEY_DESCRIPTION, p.getDescription());
+    } else {
+      rc.unset(PROJECT, null, KEY_DESCRIPTION);
+    }
+    set(rc, ACCESS, null, KEY_INHERIT_FROM, p.getParentName());
+
+    set(rc, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, p.isUseContributorAgreements());
+    set(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, p.isUseSignedOffBy());
+    set(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, p.isRequireChangeID());
+
+    set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), defaultSubmitAction);
+    set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.isUseContentMerge());
+
+    Set<AccountGroup.UUID> keepGroups = new HashSet<AccountGroup.UUID>();
+    for (AccessSection as : sort(accessSections.values())) {
+      String refName = as.getRefPattern();
+
+      StringBuilder doNotInherit = new StringBuilder();
+      for (Permission perm : sort(as.getPermissions())) {
+        if (perm.getExclusiveGroup()) {
+          if (0 < doNotInherit.length()) {
+            doNotInherit.append(' ');
+          }
+          doNotInherit.append(perm.getName());
+        }
+      }
+      if (0 < doNotInherit.length()) {
+        rc.setString(ACCESS, refName, KEY_GROUP_PERMISSIONS, doNotInherit.toString());
+      } else {
+        rc.unset(ACCESS, refName, KEY_GROUP_PERMISSIONS);
+      }
+
+      Set<String> have = new HashSet<String>();
+      for (Permission permission : sort(as.getPermissions())) {
+        have.add(permission.getName().toLowerCase());
+
+        boolean needRange = permission.isLabel();
+        List<String> rules = new ArrayList<String>();
+        for (PermissionRule rule : sort(permission.getRules())) {
+          GroupReference group = rule.getGroup();
+          if (group.getUUID() != null) {
+            keepGroups.add(group.getUUID());
+          }
+          rules.add(rule.asString(needRange));
+        }
+        rc.setStringList(ACCESS, refName, permission.getName(), rules);
+      }
+
+      for (String varName : rc.getNames(ACCESS, refName)) {
+        if (isPermission(varName) && !have.contains(varName.toLowerCase())) {
+          rc.unset(ACCESS, refName, varName);
+        }
+      }
+    }
+
+    for (String name : rc.getSubsections(ACCESS)) {
+      if (isAccessSection(name) && !accessSections.containsKey(name)) {
+        rc.unsetSection(ACCESS, name);
+      }
+    }
+    groupsByUUID.keySet().retainAll(keepGroups);
+
+    saveConfig(PROJECT_CONFIG, rc);
+    saveGroupList();
+  }
+
+  private void saveGroupList() throws IOException {
+    if (groupsByUUID.isEmpty()) {
+      saveFile(GROUP_LIST, null);
+      return;
+    }
+
+    final int uuidLen = 40;
+    StringBuilder buf = new StringBuilder();
+    buf.append(pad(uuidLen, "# UUID"));
+    buf.append('\t');
+    buf.append("Group Name");
+    buf.append('\n');
+
+    buf.append('#');
+    buf.append('\n');
+
+    for (GroupReference g : sort(groupsByUUID.values())) {
+      if (g.getUUID() != null && g.getName() != null) {
+        buf.append(pad(uuidLen, g.getUUID().get()));
+        buf.append('\t');
+        buf.append(g.getName());
+        buf.append('\n');
+      }
+    }
+    saveUTF8(GROUP_LIST, buf.toString());
+  }
+
+  private static String pad(int len, String src) {
+    if (len <= src.length()) {
+      return src;
+    }
+
+    StringBuilder r = new StringBuilder(len);
+    r.append(src);
+    while (r.length() < len) {
+      r.append(' ');
+    }
+    return r.toString();
+  }
+
+  private static <T extends Comparable<? super T>> List<T> sort(Collection<T> m) {
+    ArrayList<T> r = new ArrayList<T>(m);
+    Collections.sort(r);
+    return r;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java
index 8985f29..b5f991c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java
@@ -15,10 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.config.WildProjectName;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -37,20 +34,16 @@
   private static final Logger log =
       LoggerFactory.getLogger(PushAllProjectsOp.class);
 
-  private final SchemaFactory<ReviewDb> schema;
+  private final ProjectCache projectCache;
   private final ReplicationQueue replication;
-  private final Project.NameKey wildProject;
   private final String urlMatch;
 
   @Inject
-  public PushAllProjectsOp(final WorkQueue wq,
-      final SchemaFactory<ReviewDb> sf, final ReplicationQueue rq,
-      @WildProjectName final Project.NameKey wp,
-      @Assisted @Nullable final String urlMatch) {
+  public PushAllProjectsOp(final WorkQueue wq, final ProjectCache projectCache,
+      final ReplicationQueue rq, @Assisted @Nullable final String urlMatch) {
     super(wq);
-    this.schema = sf;
+    this.projectCache = projectCache;
     this.replication = rq;
-    this.wildProject = wp;
     this.urlMatch = urlMatch;
   }
 
@@ -63,17 +56,10 @@
 
   public void run() {
     try {
-      final ReviewDb db = schema.open();
-      try {
-        for (final Project project : db.projects().all()) {
-          if (!project.getNameKey().equals(wildProject)) {
-            replication.scheduleFullSync(project.getNameKey(), urlMatch);
-          }
-        }
-      } finally {
-        db.close();
+      for (final Project.NameKey nameKey : projectCache.all()) {
+        replication.scheduleFullSync(nameKey, urlMatch);
       }
-    } catch (OrmException e) {
+    } catch (RuntimeException e) {
       log.error("Cannot enumerate known projects", e);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
index fd7649a..cb005aa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
@@ -356,7 +356,7 @@
 
       String[] authGroupNames =
           cfg.getStringList("remote", rc.getName(), "authGroup");
-      final Set<AccountGroup.Id> authGroups;
+      final Set<AccountGroup.UUID> authGroups;
       if (authGroupNames.length > 0) {
         authGroups = ConfigUtil.groupsFor(db, authGroupNames, //
             log, "Group \"{0}\" not in database, removing from authGroup");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 4380ff0..97ad546 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -40,6 +40,7 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.mail.CreateChangeSender;
@@ -48,7 +49,9 @@
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
+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.gwtorm.client.AtomicUpdate;
 import com.google.gwtorm.client.OrmException;
@@ -141,6 +144,9 @@
   private final ReplicationQueue replication;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final ChangeHookRunner hooks;
+  private final GitRepositoryManager repoManager;
+  private final ProjectCache projectCache;
+  private final GroupCache groupCache;
   private final String canonicalWebUrl;
   private final PersonIdent gerritIdent;
   private final TrackingFooters trackingFooters;
@@ -175,6 +181,9 @@
       final ReplicationQueue replication,
       final PatchSetInfoFactory patchSetInfoFactory,
       final ChangeHookRunner hooks,
+      final ProjectCache projectCache,
+      final GitRepositoryManager repoManager,
+      final GroupCache groupCache,
       @CanonicalWebUrl @Nullable final String canonicalWebUrl,
       @GerritPersonIdent final PersonIdent gerritIdent,
       final TrackingFooters trackingFooters,
@@ -191,6 +200,9 @@
     this.replication = replication;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.hooks = hooks;
+    this.projectCache = projectCache;
+    this.repoManager = repoManager;
+    this.groupCache = groupCache;
     this.canonicalWebUrl = canonicalWebUrl;
     this.gerritIdent = gerritIdent;
     this.trackingFooters = trackingFooters;
@@ -376,6 +388,13 @@
           }
         }
 
+        if (isConfig(c)) {
+          projectCache.evict(project);
+          ProjectState ps = projectCache.get(project.getNameKey());
+          repoManager.setProjectDescription(project.getNameKey(), //
+              ps.getProject().getDescription());
+        }
+
         if (!c.getRefName().startsWith(NEW_CHANGE)) {
           // We only schedule direct refs updates for replication.
           // Change refs are scheduled when they are created.
@@ -402,9 +421,14 @@
     AbstractAgreement bestAgreement = null;
     ContributorAgreement bestCla = null;
 
-    OUTER: for (AccountGroup.Id groupId : currentUser.getEffectiveGroups()) {
+    OUTER: for (AccountGroup.UUID groupUUID : currentUser.getEffectiveGroups()) {
+      AccountGroup group = groupCache.get(groupUUID);
+      if (group == null) {
+        continue;
+      }
+
       final List<AccountGroupAgreement> temp =
-          db.accountGroupAgreements().byGroup(groupId).toList();
+          db.accountGroupAgreements().byGroup(group.getId()).toList();
 
       Collections.reverse(temp);
 
@@ -555,24 +579,59 @@
       switch (cmd.getType()) {
         case CREATE:
           parseCreate(cmd);
-          continue;
+          break;
 
         case UPDATE:
           parseUpdate(cmd);
-          continue;
+          break;
 
         case DELETE:
           parseDelete(cmd);
-          continue;
+          break;
 
         case UPDATE_NONFASTFORWARD:
           parseRewind(cmd);
+          break;
+
+        default:
+          reject(cmd);
           continue;
       }
 
-      // Everything else is bogus as far as we are concerned.
-      //
-      reject(cmd);
+      if (cmd.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED){
+        continue;
+      }
+
+      if (isConfig(cmd)) {
+        if (!projectControl.isOwner()) {
+          reject(cmd, "not project owner");
+          continue;
+        }
+
+        switch (cmd.getType()) {
+          case CREATE:
+          case UPDATE:
+          case UPDATE_NONFASTFORWARD:
+            try {
+              ProjectConfig cfg = new ProjectConfig(project.getNameKey());
+              cfg.load(repo, cmd.getNewId());
+            } catch (Exception e) {
+              reject(cmd, "invalid project configuration");
+              log.error("User " + currentUser.getUserName()
+                  + " tried to push invalid project configuration "
+                  + cmd.getNewId().name() + " for " + project.getName(), e);
+              continue;
+            }
+            break;
+
+          case DELETE:
+            break;
+
+          default:
+            reject(cmd);
+            continue;
+        }
+      }
     }
   }
 
@@ -1299,15 +1358,18 @@
         oldCC.add(a.getAccountId());
       }
 
-      final ApprovalType type =
-          approvalTypes.getApprovalType(a.getCategoryId());
-      if (a.getPatchSetId().equals(priorPatchSet)
-          && type.getCategory().isCopyMinScore() && type.isMaxNegative(a)) {
-        // If there was a negative vote on the prior patch set, carry it
-        // into this patch set.
-        //
-        db.patchSetApprovals().insert(
-            Collections.singleton(new PatchSetApproval(ps.getId(), a)));
+      // ApprovalCategory.SUBMIT is still in db but not relevant in git-store
+      if (!ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
+        final ApprovalType type =
+          approvalTypes.byId(a.getCategoryId());
+        if (a.getPatchSetId().equals(priorPatchSet)
+            && type.getCategory().isCopyMinScore() && type.isMaxNegative(a)) {
+          // If there was a negative vote on the prior patch set, carry it
+          // into this patch set.
+          //
+          db.patchSetApprovals().insert(
+              Collections.singleton(new PatchSetApproval(ps.getId(), a)));
+        }
       }
 
       if (!haveAuthor && authorId != null && a.getAccountId().equals(authorId)) {
@@ -1929,4 +1991,9 @@
   private static boolean isHead(final ReceiveCommand cmd) {
     return cmd.getRefName().startsWith(Constants.R_HEADS);
   }
+
+  private static boolean isConfig(final ReceiveCommand cmd) {
+    return cmd.getRefName().equals(GitRepositoryManager.REF_CONFIG);
+  }
+
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
new file mode 100644
index 0000000..cedcb0b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
@@ -0,0 +1,153 @@
+// 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;
+
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.Project.NameKey;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class RenameGroupOp extends DefaultQueueOp {
+  public interface Factory {
+    RenameGroupOp create(@Assisted("author") PersonIdent author,
+        @Assisted AccountGroup.UUID uuid, @Assisted("oldName") String oldName,
+        @Assisted("newName") String newName);
+  }
+
+  private static final int MAX_TRIES = 10;
+  private static final Logger log =
+      LoggerFactory.getLogger(RenameGroupOp.class);
+
+  private final ProjectCache projectCache;
+  private final MetaDataUpdate.Server metaDataUpdateFactory;
+
+  private final PersonIdent author;
+  private final AccountGroup.UUID uuid;
+  private final String oldName;
+  private final String newName;
+  private final List<Project.NameKey> retryOn;
+
+  private boolean tryingAgain;
+
+  @Inject
+  public RenameGroupOp(WorkQueue workQueue, ProjectCache projectCache,
+      MetaDataUpdate.Server metaDataUpdateFactory,
+
+      @Assisted("author") PersonIdent author, @Assisted AccountGroup.UUID uuid,
+      @Assisted("oldName") String oldName, @Assisted("newName") String newName) {
+    super(workQueue);
+    this.projectCache = projectCache;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+
+    this.author = author;
+    this.uuid = uuid;
+    this.oldName = oldName;
+    this.newName = newName;
+    this.retryOn = new ArrayList<Project.NameKey>();
+  }
+
+  @Override
+  public void run() {
+    Iterable<NameKey> names = tryingAgain ? retryOn : projectCache.all();
+    for (Project.NameKey projectName : names) {
+      ProjectConfig config = projectCache.get(projectName).getConfig();
+      GroupReference ref = config.getGroup(uuid);
+      if (ref == null || newName.equals(ref.getName())) {
+        continue;
+      }
+
+      try {
+        MetaDataUpdate md = metaDataUpdateFactory.create(projectName);
+        try {
+          rename(md);
+        } finally {
+          md.close();
+        }
+      } catch (RepositoryNotFoundException noProject) {
+        continue;
+      } catch (ConfigInvalidException err) {
+        log.error("Cannot rename group " + oldName + " in " + projectName, err);
+      } catch (IOException err) {
+        log.error("Cannot rename group " + oldName + " in " + projectName, err);
+      }
+    }
+
+    // If one or more projects did not update, wait 5 minutes
+    // and give it another attempt.
+    if (!retryOn.isEmpty() && !tryingAgain) {
+      tryingAgain = true;
+      start(5, TimeUnit.MINUTES);
+    }
+  }
+
+  private void rename(MetaDataUpdate md) throws IOException,
+      ConfigInvalidException {
+    boolean success = false;
+    for (int attempts = 0; !success && attempts < MAX_TRIES; attempts++) {
+      ProjectConfig config = ProjectConfig.read(md);
+
+      // The group isn't referenced, or its name has been fixed already.
+      //
+      GroupReference ref = config.getGroup(uuid);
+      if (ref == null || newName.equals(ref.getName())) {
+        projectCache.evict(config.getProject());
+        return;
+      }
+
+      ref.setName(newName);
+      md.getCommitBuilder().setAuthor(author);
+      md.setMessage("Rename group " + oldName + " to " + newName + "\n");
+      if (config.commit(md)) {
+        projectCache.evict(config.getProject());
+        success = true;
+
+      } else {
+        try {
+          Thread.sleep(25 /* milliseconds */);
+        } catch (InterruptedException wakeUp) {
+          continue;
+        }
+      }
+    }
+
+    if (!success) {
+      if (tryingAgain) {
+        log.warn("Could not rename group " + oldName + " to " + newName
+            + " in " + md.getProjectName().get());
+      } else {
+        retryOn.add(md.getProjectName());
+      }
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "Rename Group " + oldName;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteHeaderFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteHeaderFormatter.java
index d54dcec..8997e2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteHeaderFormatter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteHeaderFormatter.java
@@ -51,8 +51,7 @@
 
   void appendApproval(ApprovalCategory category,
       short value, Account user) {
-    // TODO: use category.getLabel() when available
-    sb.append(category.getName().replace(' ', '-'));
+    sb.append(category.getLabelName());
     sb.append(value < 0 ? "-" : "+").append(Math.abs(value)).append(": ");
     appendUserData(user);
     sb.append("\n");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
new file mode 100644
index 0000000..bb91044
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -0,0 +1,318 @@
+// Copyright (C) 2010 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;
+
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
+import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.UnmergedPathException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.RawParseUtils;
+
+import java.io.IOException;
+
+/**
+ * Support for metadata stored within a version controlled branch.
+ * <p>
+ * Implementors are responsible for supplying implementations of the onLoad and
+ * onSave methods to read from the repository, or format an update that can
+ * later be written back to the repository.
+ */
+public abstract class VersionedMetaData {
+  private RevCommit revision;
+  private ObjectReader reader;
+  private ObjectInserter inserter;
+  private DirCache newTree;
+
+  /** @return name of the reference storing this configuration. */
+  protected abstract String getRefName();
+
+  protected abstract void onLoad() throws IOException, ConfigInvalidException;
+
+  protected abstract void onSave(CommitBuilder commit) throws IOException,
+      ConfigInvalidException;
+
+  /** @return revision of the metadata that was loaded. */
+  public ObjectId getRevision() {
+    return revision.copy();
+  }
+
+  /** Initialize in-memory as though the repository branch doesn't exist. */
+  public void createInMemory() {
+    try {
+      revision = null;
+      onLoad();
+    } catch (IOException err) {
+      throw new RuntimeException("Unexpected IOException", err);
+    } catch (ConfigInvalidException err) {
+      throw new RuntimeException("Unexpected ConfigInvalidException", err);
+    }
+  }
+
+  /**
+   * Load the current version from the branch.
+   * <p>
+   * The repository is not held after the call completes, allowing the
+   * application to retain this object for long periods of time.
+   *
+   * @param db repository to access.
+   * @throws IOException
+   * @throws ConfigInvalidException
+   */
+  public void load(Repository db) throws IOException, ConfigInvalidException {
+    Ref ref = db.getRef(getRefName());
+    load(db, ref != null ? ref.getObjectId() : null);
+  }
+
+  /**
+   * Load a specific version from the repository.
+   * <p>
+   * This method is primarily useful for applying updates to a specific revision
+   * that was shown to an end-user in the user interface. If there are conflicts
+   * with another user's concurrent changes, these will be automatically
+   * detected at commit time.
+   * <p>
+   * The repository is not held after the call completes, allowing the
+   * application to retain this object for long periods of time.
+   *
+   * @param db repository to access.
+   * @param id revision to load.
+   * @throws IOException
+   * @throws ConfigInvalidException
+   */
+  public void load(Repository db, ObjectId id) throws IOException,
+      ConfigInvalidException {
+    if (id != null) {
+      reader = db.newObjectReader();
+      try {
+        revision = new RevWalk(reader).parseCommit(id);
+        onLoad();
+      } finally {
+        reader.release();
+        reader = null;
+      }
+    } else {
+      // The branch does not yet exist.
+      revision = null;
+      onLoad();
+    }
+  }
+
+  public void load(MetaDataUpdate update) throws IOException,
+      ConfigInvalidException {
+    load(update.getRepository());
+  }
+
+  public void load(MetaDataUpdate update, ObjectId id) throws IOException,
+      ConfigInvalidException {
+    load(update.getRepository(), id);
+  }
+
+  /**
+   * Update this metadata branch, recording a new commit on its reference.
+   *
+   * @param update helper information to define the update that will occur.
+   * @return true if the update was successful, false if it failed because of a
+   *         concurrent update to the same reference.
+   * @throws IOException if there is a storage problem and the update cannot be
+   *         executed as requested.
+   */
+  public boolean commit(MetaDataUpdate update) throws IOException {
+    final Repository db = update.getRepository();
+    final CommitBuilder commit = update.getCommitBuilder();
+
+    reader = db.newObjectReader();
+    inserter = db.newObjectInserter();
+    try {
+      final RevWalk rw = new RevWalk(reader);
+      final RevTree src = revision != null ? rw.parseTree(revision) : null;
+      final ObjectId res = writeTree(src, commit);
+
+      if (res.equals(src)) {
+        // If there are no changes to the content, don't create the commit.
+        return true;
+      }
+
+      commit.setTreeId(res);
+      if (revision != null) {
+        commit.setParentId(revision);
+      }
+
+      RefUpdate ru = db.updateRef(getRefName());
+      if (revision != null) {
+        ru.setExpectedOldObjectId(revision);
+      } else {
+        ru.setExpectedOldObjectId(ObjectId.zeroId());
+      }
+      ru.setNewObjectId(inserter.insert(commit));
+      ru.disableRefLog();
+      inserter.flush();
+
+      switch (ru.update(rw)) {
+        case NEW:
+        case FAST_FORWARD:
+          revision = rw.parseCommit(ru.getNewObjectId());
+          update.replicate(ru.getName());
+          return true;
+
+        case LOCK_FAILURE:
+          return false;
+
+        default:
+          throw new IOException("Cannot update " + ru.getName() + " in "
+              + db.getDirectory() + ": " + ru.getResult());
+      }
+    } catch (ConfigInvalidException e) {
+      throw new IOException("Cannot update " + getRefName() + " in "
+          + db.getDirectory() + ": " + e.getMessage(), e);
+    } finally {
+      inserter.release();
+      inserter = null;
+
+      reader.release();
+      reader = null;
+    }
+  }
+
+  private ObjectId writeTree(RevTree srcTree, CommitBuilder commit)
+      throws IOException, MissingObjectException, IncorrectObjectTypeException,
+      UnmergedPathException, ConfigInvalidException {
+    try {
+      newTree = readTree(srcTree);
+      onSave(commit);
+      return newTree.writeTree(inserter);
+    } finally {
+      newTree = null;
+    }
+  }
+
+  private DirCache readTree(RevTree tree) throws IOException,
+      MissingObjectException, IncorrectObjectTypeException {
+    DirCache dc = DirCache.newInCore();
+    if (tree != null) {
+      DirCacheBuilder b = dc.builder();
+      b.addTree(new byte[0], DirCacheEntry.STAGE_0, reader, tree);
+      b.finish();
+    }
+    return dc;
+  }
+
+  protected Config readConfig(String fileName) throws IOException,
+      ConfigInvalidException {
+    Config rc = new Config();
+    String text = readUTF8(fileName);
+    if (!text.isEmpty()) {
+      try {
+        rc.fromText(text);
+      } catch (ConfigInvalidException err) {
+        throw new ConfigInvalidException("Invalid config file " + fileName
+            + " in commit" + revision.name(), err);
+      }
+    }
+    return rc;
+  }
+
+  protected String readUTF8(String fileName) throws IOException {
+    byte[] raw = readFile(fileName);
+    return raw.length != 0 ? RawParseUtils.decode(raw) : "";
+  }
+
+  protected byte[] readFile(String fileName) throws IOException {
+    if (revision == null) {
+      return new byte[] {};
+    }
+
+    TreeWalk tw = TreeWalk.forPath(reader, fileName, revision.getTree());
+    if (tw != null) {
+      ObjectLoader obj = reader.open(tw.getObjectId(0), Constants.OBJ_BLOB);
+      return obj.getCachedBytes(Integer.MAX_VALUE);
+
+    } else {
+      return new byte[] {};
+    }
+  }
+
+  protected static void set(Config rc, String section, String subsection,
+      String name, String value) {
+    if (value != null) {
+      rc.setString(section, subsection, name, value);
+    } else {
+      rc.unset(section, subsection, name);
+    }
+  }
+
+  protected static void set(Config rc, String section, String subsection,
+      String name, boolean value) {
+    if (value) {
+      rc.setBoolean(section, subsection, name, value);
+    } else {
+      rc.unset(section, subsection, name);
+    }
+  }
+
+  protected static <E extends Enum<?>> void set(Config rc, String section,
+      String subsection, String name, E value, E defaultValue) {
+    if (value != defaultValue) {
+      rc.setEnum(section, subsection, name, value);
+    } else {
+      rc.unset(section, subsection, name);
+    }
+  }
+
+  protected void saveConfig(String fileName, Config cfg) throws IOException {
+    saveUTF8(fileName, cfg.toText());
+  }
+
+  protected void saveUTF8(String fileName, String text) throws IOException {
+    saveFile(fileName, text != null ? Constants.encode(text) : null);
+  }
+
+  protected void saveFile(String fileName, byte[] raw) throws IOException {
+    DirCacheEditor editor = newTree.editor();
+    if (raw != null && 0 < raw.length) {
+      final ObjectId blobId = inserter.insert(Constants.OBJ_BLOB, raw);
+      editor.add(new PathEdit(fileName) {
+        @Override
+        public void apply(DirCacheEntry ent) {
+          ent.setFileMode(FileMode.REGULAR_FILE);
+          ent.setObjectId(blobId);
+        }
+      });
+    } else {
+      editor.add(new DeletePath(fileName));
+    }
+    editor.finish();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index f5a7870..ebbe686 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -69,8 +69,8 @@
 
     /** Is the from user in an email squelching group? */
     final IdentifiedUser user =  args.identifiedUserFactory.create(id);
-    final Set<AccountGroup.Id> gids = user.getEffectiveGroups();
-    for (final AccountGroup.Id gid : gids) {
+    final Set<AccountGroup.UUID> gids = user.getEffectiveGroups();
+    for (final AccountGroup.UUID gid : gids) {
       if (args.groupCache.get(gid).isEmailOnlyAuthors()) {
         emailOnlyAuthors = true;
         break;
@@ -273,11 +273,11 @@
   }
 
   /** Get the groups which own the project. */
-  protected Set<AccountGroup.Id> getProjectOwners() {
+  protected Set<AccountGroup.UUID> getProjectOwners() {
     final ProjectState r;
 
     r = args.projectCache.get(change.getProject());
-    return r != null ? r.getOwners() : Collections.<AccountGroup.Id> emptySet();
+    return r != null ? r.getOwners() : Collections.<AccountGroup.UUID> emptySet();
   }
 
   /** TO or CC all vested parties (change owner, patch set uploader, author). */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
index c14ff1b..8bb68df 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.AccountProjectWatch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.AccountProjectWatch.NotifyType;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
@@ -34,10 +35,13 @@
     public CreateChangeSender create(Change change);
   }
 
+  private final GroupCache groupCache;
+
   @Inject
   public CreateChangeSender(EmailArguments ea, SshInfo sshInfo,
-      @Assisted Change c) {
+      GroupCache groupCache, @Assisted Change c) {
     super(ea, sshInfo, c);
+    this.groupCache = groupCache;
   }
 
   @Override
@@ -52,10 +56,13 @@
       // Try to mark interested owners with a TO and not a BCC line.
       //
       final Set<Account.Id> owners = new HashSet<Account.Id>();
-      for (AccountGroup.Id g : getProjectOwners()) {
-        for (AccountGroupMember m : args.db.get().accountGroupMembers()
-            .byGroup(g)) {
-          owners.add(m.getAccountId());
+      for (AccountGroup.UUID uuid : getProjectOwners()) {
+        AccountGroup group = groupCache.get(uuid);
+        if (group != null) {
+          for (AccountGroupMember m : args.db.get().accountGroupMembers()
+              .byGroup(group.getId())) {
+            owners.add(m.getAccountId());
+          }
         }
       }
 
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 f3e2890..db297ae 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
@@ -171,7 +171,9 @@
       final short o = a.getValue();
       a.setValue(want.get());
       a.cache(change);
-      functionState.normalize(types.getApprovalType(a.getCategoryId()), a);
+      if (!ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
+        functionState.normalize(types.byId(a.getCategoryId()), a);
+      }
       if (o != a.getValue()) {
         // Value changed, ensure we update the database.
         //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
index 1e2e7f4..64e5299 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
@@ -33,19 +33,19 @@
 public class AccessControlModule extends FactoryModule {
   @Override
   protected void configure() {
-    bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) //
+    bind(new TypeLiteral<Set<AccountGroup.UUID>>() {}) //
         .annotatedWith(ProjectCreatorGroups.class) //
         .toProvider(ProjectCreatorGroupsProvider.class).in(SINGLETON);
 
-    bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) //
+    bind(new TypeLiteral<Set<AccountGroup.UUID>>() {}) //
         .annotatedWith(ProjectOwnerGroups.class) //
         .toProvider(ProjectOwnerGroupsProvider.class).in(SINGLETON);
 
-    bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) //
+    bind(new TypeLiteral<Set<AccountGroup.UUID>>() {}) //
         .annotatedWith(GitUploadPackGroups.class) //
         .toProvider(GitUploadPackGroupsProvider.class).in(SINGLETON);
 
-    bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) //
+    bind(new TypeLiteral<Set<AccountGroup.UUID>>() {}) //
         .annotatedWith(GitReceivePackGroups.class) //
         .toProvider(GitReceivePackGroupsProvider.class).in(SINGLETON);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 3291f1a..b387e70 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -16,13 +16,12 @@
 
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.reviewdb.ApprovalCategory;
+import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.workflow.CategoryFunction;
@@ -31,7 +30,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import java.util.ArrayList;
 import java.util.List;
 
 
@@ -164,8 +162,14 @@
     return canAbandon(); // Anyone who can abandon the change can restore it back
   }
 
-  public short normalize(ApprovalCategory.Id category, short score) {
-    return getRefControl().normalize(category, score);
+  /** All value ranges of any allowed label permission. */
+  public List<PermissionRange> getLabelRanges() {
+    return getRefControl().getLabelRanges();
+  }
+
+  /** The range of permitted values associated with a label permission. */
+  public PermissionRange getRange(String permission) {
+    return getRefControl().getRange(permission);
   }
 
   /** Can this user add a patch set to this change? */
@@ -240,34 +244,22 @@
       return result;
     }
 
-    final List<PatchSetApproval> allApprovals =
-        new ArrayList<PatchSetApproval>(db.patchSetApprovals().byPatchSet(
-            patchSetId).toList());
-    final PatchSetApproval myAction =
-        ChangeUtil.createSubmitApproval(patchSetId,
-            (IdentifiedUser) getCurrentUser(), db);
-
-    final ApprovalType actionType =
-        approvalTypes.getApprovalType(myAction.getCategoryId());
-    if (actionType == null || !actionType.getCategory().isAction()) {
-      return new CanSubmitResult("Invalid action " + myAction.getCategoryId());
-    }
+    final List<PatchSetApproval> all =
+        db.patchSetApprovals().byPatchSet(patchSetId).toList();
 
     final FunctionState fs =
-        functionStateFactory.create(change, patchSetId, allApprovals);
+        functionStateFactory.create(change, patchSetId, all);
+
     for (ApprovalType c : approvalTypes.getApprovalTypes()) {
       CategoryFunction.forCategory(c.getCategory()).run(c, fs);
     }
-    if (!CategoryFunction.forCategory(actionType.getCategory()).isValid(
-        getCurrentUser(), actionType, fs)) {
-      return new CanSubmitResult(actionType.getCategory().getName()
-          + " not permitted");
+
+    for (ApprovalType type : approvalTypes.getApprovalTypes()) {
+      if (!fs.isValid(type)) {
+        return new CanSubmitResult("Requires " + type.getCategory().getName());
+      }
     }
-    fs.normalize(actionType, myAction);
-    if (myAction.getValue() <= 0) {
-      return new CanSubmitResult(actionType.getCategory().getName()
-          + " not permitted");
-    }
+
     return CanSubmitResult.OK;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/InvalidChangeOperationException.java
similarity index 64%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/project/InvalidChangeOperationException.java
index 0977ee9..b288b10 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/InvalidChangeOperationException.java
@@ -12,15 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.server.project;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.gerrit.reviewdb.Change;
 
-public class Schema_49 extends SchemaVersion {
+/** Indicates the change operation is not currently valid. */
+public class InvalidChangeOperationException extends Exception {
+  private static final long serialVersionUID = 1L;
 
-  @Inject
-  Schema_49(Provider<Schema_48> prior) {
-    super(prior);
+  public InvalidChangeOperationException(String msg) {
+    super(msg);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
index 35b5ee5..4202a62 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
@@ -29,6 +29,17 @@
   /** Invalidate the cached information about the given project. */
   public void evict(Project p);
 
-  /** Invalidate the cached information about all projects. */
-  public void evictAll();
+  /** @return sorted iteration of projects. */
+  public abstract Iterable<Project.NameKey> all();
+
+  /**
+   * Filter the set of registered project names by common prefix.
+   *
+   * @param prefix common prefix.
+   * @return sorted iteration of projects sharing the same prefix.
+   */
+  public abstract Iterable<Project.NameKey> byName(String prefix);
+
+  /** Notify the cache that a new project was constructed. */
+  public void onCreateProject(Project.NameKey newProjectName);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 48eef87..3990d06 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -15,11 +15,14 @@
 package com.google.gerrit.server.project;
 
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -27,21 +30,38 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
-import java.util.Collection;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+
 import java.util.Collections;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 /** Cache of project information, including access rights. */
 @Singleton
 public class ProjectCacheImpl implements ProjectCache {
   private static final String CACHE_NAME = "projects";
+  private static final String CACHE_LIST = "project_list";
 
   public static Module module() {
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<Project.NameKey, ProjectState>> type =
+        final TypeLiteral<Cache<Project.NameKey, ProjectState>> nameType =
             new TypeLiteral<Cache<Project.NameKey, ProjectState>>() {};
-        core(type, CACHE_NAME).populateWith(Loader.class);
+        core(nameType, CACHE_NAME).populateWith(Loader.class);
+
+        final TypeLiteral<Cache<ListKey, SortedSet<Project.NameKey>>> listType =
+            new TypeLiteral<Cache<ListKey, SortedSet<Project.NameKey>>>() {};
+        core(listType, CACHE_LIST).populateWith(Lister.class);
+
         bind(ProjectCacheImpl.class);
         bind(ProjectCache.class).to(ProjectCacheImpl.class);
       }
@@ -49,11 +69,39 @@
   }
 
   private final Cache<Project.NameKey, ProjectState> byName;
+  private final Cache<ListKey,SortedSet<Project.NameKey>> list;
+  private final Lock listLock;
+  private volatile long generation;
 
   @Inject
   ProjectCacheImpl(
-      @Named(CACHE_NAME) final Cache<Project.NameKey, ProjectState> byName) {
+      @Named(CACHE_NAME) final Cache<Project.NameKey, ProjectState> byName,
+      @Named(CACHE_LIST) final Cache<ListKey, SortedSet<Project.NameKey>> list,
+      @GerritServerConfig final Config serverConfig) {
     this.byName = byName;
+    this.list = list;
+    this.listLock = new ReentrantLock(true /* fair */);
+
+    long checkFrequencyMillis = TimeUnit.MILLISECONDS.convert(
+        ConfigUtil.getTimeUnit(serverConfig,
+            "cache", "projects", "checkFrequency",
+            5, TimeUnit.MINUTES), TimeUnit.MINUTES);
+    if (10 < checkFrequencyMillis) {
+      // Start with generation 1 (to avoid magic 0 below).
+      generation = 1;
+      Executors.newScheduledThreadPool(1).scheduleAtFixedRate(new Runnable() {
+        @Override
+        public void run() {
+          // This is not exactly thread-safe, but is OK for our use.
+          // The only thread that writes the volatile is this task.
+          generation = generation + 1;
+        }
+      }, checkFrequencyMillis, checkFrequencyMillis, TimeUnit.MILLISECONDS);
+    } else {
+      // Magic generation 0 triggers ProjectState to always
+      // check on each needsRefresh() request we make to it.
+      generation = 0;
+    }
   }
 
   /**
@@ -63,7 +111,12 @@
    * @return the cached data; null if no such project exists.
    */
   public ProjectState get(final Project.NameKey projectName) {
-    return byName.get(projectName);
+    ProjectState state = byName.get(projectName);
+    if (state != null && state.needsRefresh(generation)) {
+      byName.remove(projectName);
+      state = byName.get(projectName);
+    }
+    return state;
   }
 
   /** Invalidate the cached information about the given project. */
@@ -73,38 +126,127 @@
     }
   }
 
-  /** Invalidate the cached information about all projects. */
-  public void evictAll() {
-    byName.removeAll();
+  @Override
+  public void onCreateProject(Project.NameKey newProjectName) {
+    listLock.lock();
+    try {
+      SortedSet<Project.NameKey> n = list.get(ListKey.ALL);
+      n = new TreeSet<Project.NameKey>(n);
+      n.add(newProjectName);
+      list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
+    } finally {
+      listLock.unlock();
+    }
+  }
+
+  @Override
+  public Iterable<Project.NameKey> all() {
+    return list.get(ListKey.ALL);
+  }
+
+  @Override
+  public Iterable<Project.NameKey> byName(final String pfx) {
+    return new Iterable<Project.NameKey>() {
+      @Override
+      public Iterator<Project.NameKey> iterator() {
+        return new Iterator<Project.NameKey>() {
+          private Project.NameKey next;
+          private Iterator<Project.NameKey> itr =
+              list.get(ListKey.ALL).tailSet(new Project.NameKey(pfx)).iterator();
+
+          @Override
+          public boolean hasNext() {
+            if (next != null) {
+              return true;
+            }
+
+            if (!itr.hasNext()) {
+              return false;
+            }
+
+            Project.NameKey r = itr.next();
+            if (r.get().startsWith(pfx)) {
+              next = r;
+              return true;
+            } else {
+              itr = Collections.<Project.NameKey> emptyList().iterator();
+              return false;
+            }
+          }
+
+          @Override
+          public Project.NameKey next() {
+            if (!hasNext()) {
+              throw new NoSuchElementException();
+            }
+
+            Project.NameKey r = next;
+            next = null;
+            return r;
+          }
+
+          @Override
+          public void remove() {
+            throw new UnsupportedOperationException();
+          }
+        };
+      }
+    };
   }
 
   static class Loader extends EntryCreator<Project.NameKey, ProjectState> {
     private final ProjectState.Factory projectStateFactory;
     private final SchemaFactory<ReviewDb> schema;
+    private final GitRepositoryManager mgr;
 
     @Inject
-    Loader(ProjectState.Factory psf, SchemaFactory<ReviewDb> sf) {
+    Loader(ProjectState.Factory psf, SchemaFactory<ReviewDb> sf,
+        GitRepositoryManager g) {
       projectStateFactory = psf;
       schema = sf;
+      mgr = g;
     }
 
     @Override
     public ProjectState createEntry(Project.NameKey key) throws Exception {
       final ReviewDb db = schema.open();
       try {
-        final Project p = db.projects().get(key);
-        if (p == null) {
-          return null;
+        Repository git = mgr.openRepository(key);
+        try {
+          final ProjectConfig cfg = new ProjectConfig(key);
+          cfg.load(git);
+          return projectStateFactory.create(cfg);
+        } finally {
+          git.close();
         }
 
-        final Collection<RefRight> rights =
-            Collections.unmodifiableCollection(db.refRights().byProject(
-                p.getNameKey()).toList());
+      } catch (RepositoryNotFoundException notFound) {
+        return null;
 
-        return projectStateFactory.create(p, rights);
       } finally {
         db.close();
       }
     }
   }
+
+  static class ListKey {
+    static final ListKey ALL = new ListKey();
+
+    private ListKey() {
+    }
+  }
+
+  static class Lister extends EntryCreator<ListKey, SortedSet<Project.NameKey>> {
+    private final GitRepositoryManager mgr;
+
+    @Inject
+    Lister(GitRepositoryManager mgr) {
+      this.mgr = mgr;
+    }
+
+    @Override
+    public SortedSet<Project.NameKey> createEntry(ListKey key) throws Exception {
+      return mgr.list();
+    }
+  }
 }
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 2a55019..74ee893 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,14 +14,15 @@
 
 package com.google.gerrit.server.project;
 
-import static com.google.gerrit.common.CollectionsUtil.*;
+import static com.google.gerrit.common.CollectionsUtil.isAnyIncludedIn;
+
+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.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.SystemConfig;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.ReplicationUser;
 import com.google.gerrit.server.config.GitReceivePackGroups;
@@ -30,6 +31,7 @@
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -103,21 +105,20 @@
     ProjectControl create(CurrentUser who, ProjectState ps);
   }
 
-  private final SystemConfig systemConfig;
-  private final Set<AccountGroup.Id> uploadGroups;
-  private final Set<AccountGroup.Id> receiveGroups;
+  private final Set<AccountGroup.UUID> uploadGroups;
+  private final Set<AccountGroup.UUID> receiveGroups;
 
   private final RefControl.Factory refControlFactory;
   private final CurrentUser user;
   private final ProjectState state;
 
+  private Collection<AccessSection> access;
+
   @Inject
-  ProjectControl(final SystemConfig systemConfig,
-      @GitUploadPackGroups Set<AccountGroup.Id> uploadGroups,
-      @GitReceivePackGroups Set<AccountGroup.Id> receiveGroups,
+  ProjectControl(@GitUploadPackGroups Set<AccountGroup.UUID> uploadGroups,
+      @GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
       final RefControl.Factory refControlFactory,
       @Assisted CurrentUser who, @Assisted ProjectState ps) {
-    this.systemConfig = systemConfig;
     this.uploadGroups = uploadGroups;
     this.receiveGroups = receiveGroups;
     this.refControlFactory = refControlFactory;
@@ -160,18 +161,18 @@
   /** Can this user see this project exists? */
   public boolean isVisible() {
     return visibleForReplication()
-        || canPerformOnAnyRef(ApprovalCategory.READ, (short) 1);
+        || canPerformOnAnyRef(Permission.READ);
   }
 
   public boolean canAddRefs() {
-    return (canPerformOnAnyRef(ApprovalCategory.PUSH_HEAD, ApprovalCategory.PUSH_HEAD_CREATE)
+    return (canPerformOnAnyRef(Permission.CREATE)
         || isOwnerAnyRef());
   }
 
   /** Can this user see all the refs in this projects? */
   public boolean allRefsAreVisible() {
     return visibleForReplication()
-        || canPerformOnAllRefs(ApprovalCategory.READ, (short) 1);
+        || canPerformOnAllRefs(Permission.READ);
   }
 
   /** Is this project completely visible for replication? */
@@ -182,65 +183,76 @@
 
   /** Is this user a project owner? Ownership does not imply {@link #isVisible()} */
   public boolean isOwner() {
-    return controlForRef(RefRight.ALL).isOwner()
+    return controlForRef(AccessSection.ALL).isOwner()
         || getCurrentUser().isAdministrator();
   }
 
   /** Does this user have ownership on at least one reference name? */
   public boolean isOwnerAnyRef() {
-    return canPerformOnAnyRef(ApprovalCategory.OWN, (short) 1)
+    return canPerformOnAnyRef(Permission.OWNER)
         || getCurrentUser().isAdministrator();
   }
 
   /** @return true if the user can upload to at least one reference */
   public boolean canPushToAtLeastOneRef() {
-    return canPerformOnAnyRef(ApprovalCategory.READ, (short) 2)
-        || canPerformOnAnyRef(ApprovalCategory.PUSH_HEAD, (short) 1)
-        || canPerformOnAnyRef(ApprovalCategory.PUSH_TAG, (short) 1);
-  }
-
-  // TODO (anatol.pomazau): Try to merge this method with similar RefRightsForPattern#canPerform
-  private boolean canPerformOnAnyRef(ApprovalCategory.Id actionId,
-      short requireValue) {
-    final Set<AccountGroup.Id> groups = getEffectiveUserGroups();
-
-    for (final RefRight pr : state.getAllRights(actionId, true)) {
-      if (groups.contains(pr.getAccountGroupId())
-          && pr.getMaxValue() >= requireValue) {
-        return true;
-      }
-    }
-
-    return false;
+    return canPerformOnAnyRef(Permission.PUSH)
+        || canPerformOnAnyRef(Permission.PUSH_TAG);
   }
 
   /**
    * @return the effective groups of the current user for this project
    */
-  private Set<AccountGroup.Id> getEffectiveUserGroups() {
-    final Set<AccountGroup.Id> userGroups = user.getEffectiveGroups();
+  private Set<AccountGroup.UUID> getEffectiveUserGroups() {
+    final Set<AccountGroup.UUID> userGroups = user.getEffectiveGroups();
     if (isOwner()) {
-      final Set<AccountGroup.Id> userGroupsOnProject =
-          new HashSet<AccountGroup.Id>(userGroups.size() + 1);
+      final Set<AccountGroup.UUID> userGroupsOnProject =
+          new HashSet<AccountGroup.UUID>(userGroups.size() + 1);
       userGroupsOnProject.addAll(userGroups);
-      userGroupsOnProject.add(systemConfig.ownerGroupId);
+      userGroupsOnProject.add(AccountGroup.PROJECT_OWNERS);
       return Collections.unmodifiableSet(userGroupsOnProject);
     } else {
       return userGroups;
     }
   }
 
-  private boolean canPerformOnAllRefs(ApprovalCategory.Id actionId,
-      short requireValue) {
+  private boolean canPerformOnAnyRef(String permissionName) {
+    final Set<AccountGroup.UUID> groups = getEffectiveUserGroups();
+
+    for (AccessSection section : access()) {
+      Permission permission = section.getPermission(permissionName);
+      if (permission == null) {
+        continue;
+      }
+
+      for (PermissionRule rule : permission.getRules()) {
+        if (rule.getDeny()) {
+          continue;
+        }
+
+        // Being in a group that was granted this permission is only an
+        // approximation.  There might be overrides and doNotInherit
+        // that would render this to be false.
+        //
+        if (groups.contains(rule.getGroup().getUUID())
+            && controlForRef(section.getRefPattern()).canPerform(permissionName)) {
+          return true;
+        }
+      }
+    }
+
+    return false;
+  }
+
+  private boolean canPerformOnAllRefs(String permission) {
     boolean canPerform = false;
-    final Set<String> patterns = allRefPatterns(actionId);
-    if (patterns.contains(RefRight.ALL)) {
+    Set<String> patterns = allRefPatterns(permission);
+    if (patterns.contains(AccessSection.ALL)) {
       // Only possible if granted on the pattern that
       // matches every possible reference.  Check all
       // patterns also have the permission.
       //
       for (final String pattern : patterns) {
-        if (controlForRef(pattern).canPerform(actionId, requireValue)) {
+        if (controlForRef(pattern).canPerform(permission)) {
           canPerform = true;
         } else {
           return false;
@@ -250,14 +262,24 @@
     return canPerform;
   }
 
-  private Set<String> allRefPatterns(ApprovalCategory.Id actionId) {
-    final Set<String> all = new HashSet<String>();
-    for (final RefRight pr : state.getAllRights(actionId, true)) {
-      all.add(pr.getRefPattern());
+  private Set<String> allRefPatterns(String permissionName) {
+    Set<String> all = new HashSet<String>();
+    for (AccessSection section : access()) {
+      Permission permission = section.getPermission(permissionName);
+      if (permission != null) {
+        all.add(section.getRefPattern());
+      }
     }
     return all;
   }
 
+  Collection<AccessSection> access() {
+    if (access == null) {
+      access = state.getAllAccessSections();
+    }
+    return access;
+  }
+
   public boolean canRunUploadPack() {
     return isAnyIncludedIn(uploadGroups, getEffectiveUserGroups());
   }
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 0b8e83a..0d297cd 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,122 +14,135 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.WildProjectName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ProjectConfig;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
 
 /** Cached information on a project. */
 public class ProjectState {
   public interface Factory {
-    ProjectState create(Project project, Collection<RefRight> localRights);
+    ProjectState create(ProjectConfig config);
   }
 
   private final AnonymousUser anonymousUser;
   private final Project.NameKey wildProject;
   private final ProjectCache projectCache;
   private final ProjectControl.AssistedFactory projectControlFactory;
+  private final GitRepositoryManager gitMgr;
 
-  private final Project project;
-  private final Collection<RefRight> localRights;
-  private final Set<AccountGroup.Id> localOwners;
+  private final ProjectConfig config;
+  private final Set<AccountGroup.UUID> localOwners;
 
-  private volatile Collection<RefRight> inheritedRights;
+  /** Last system time the configuration's revision was examined. */
+  private transient long lastCheckTime;
 
   @Inject
   protected ProjectState(final AnonymousUser anonymousUser,
       final ProjectCache projectCache,
       @WildProjectName final Project.NameKey wildProject,
       final ProjectControl.AssistedFactory projectControlFactory,
-      @Assisted final Project project,
-      @Assisted Collection<RefRight> rights) {
+      final GitRepositoryManager gitMgr,
+      @Assisted final ProjectConfig config) {
     this.anonymousUser = anonymousUser;
     this.projectCache = projectCache;
     this.wildProject = wildProject;
     this.projectControlFactory = projectControlFactory;
+    this.gitMgr = gitMgr;
+    this.config = config;
+    this.lastCheckTime = System.currentTimeMillis();
 
-    if (wildProject.equals(project.getNameKey())) {
-      rights = new ArrayList<RefRight>(rights);
-      for (Iterator<RefRight> itr = rights.iterator(); itr.hasNext();) {
-        if (!itr.next().getApprovalCategoryId().canBeOnWildProject()) {
-          itr.remove();
+    HashSet<AccountGroup.UUID> groups = new HashSet<AccountGroup.UUID>();
+    AccessSection all = config.getAccessSection(AccessSection.ALL);
+    if (all != null) {
+      Permission owner = all.getPermission(Permission.OWNER);
+      if (owner != null) {
+        for (PermissionRule rule : owner.getRules()) {
+          GroupReference ref = rule.getGroup();
+          if (ref.getUUID() != null) {
+            groups.add(ref.getUUID());
+          }
         }
       }
-      rights = Collections.unmodifiableCollection(rights);
-    }
-
-    this.project = project;
-    this.localRights = rights;
-
-    final HashSet<AccountGroup.Id> groups = new HashSet<AccountGroup.Id>();
-    for (final RefRight right : rights) {
-      if (ApprovalCategory.OWN.equals(right.getApprovalCategoryId())
-          && right.getMaxValue() > 0
-          && right.getRefPattern().equals(RefRight.ALL)) {
-        groups.add(right.getAccountGroupId());
-      }
     }
     localOwners = Collections.unmodifiableSet(groups);
   }
 
+  boolean needsRefresh(long generation) {
+    if (generation <= 0) {
+      return isRevisionOutOfDate();
+    }
+    if (lastCheckTime != generation) {
+      lastCheckTime = generation;
+      return isRevisionOutOfDate();
+    }
+    return false;
+  }
+
+  private boolean isRevisionOutOfDate() {
+    try {
+      Repository git = gitMgr.openRepository(getProject().getNameKey());
+      try {
+        Ref ref = git.getRef(GitRepositoryManager.REF_CONFIG);
+        if (ref == null || ref.getObjectId() == null) {
+          return true;
+        }
+        return !ref.getObjectId().equals(config.getRevision());
+      } finally {
+        git.close();
+      }
+    } catch (IOException gone) {
+      return true;
+    }
+  }
+
   public Project getProject() {
-    return project;
+    return getConfig().getProject();
+  }
+
+  public ProjectConfig getConfig() {
+    return config;
   }
 
   /** Get the rights that pertain only to this project. */
-  public Collection<RefRight> getLocalRights() {
-    return localRights;
+  public Collection<AccessSection> getLocalAccessSections() {
+    return getConfig().getAccessSections();
   }
 
-  /**
-   * Get the rights that pertain only to this project.
-   *
-   * @param action the category requested.
-   * @return immutable collection of rights for the requested category.
-   */
-  public Collection<RefRight> getLocalRights(ApprovalCategory.Id action) {
-    return filter(getLocalRights(), action);
-  }
-
-  /** Get the rights this project inherits from the wild project. */
-  public Collection<RefRight> getInheritedRights() {
-    if (inheritedRights == null) {
-      inheritedRights = computeInheritedRights();
-    }
-    return inheritedRights;
-  }
-
-  void setInheritedRights(Collection<RefRight> all) {
-    inheritedRights = all;
-  }
-
-  private Collection<RefRight> computeInheritedRights() {
-    if (isSpecialWildProject()) {
+  /** Get the rights this project inherits. */
+  public Collection<AccessSection> getInheritedAccessSections() {
+    if (isWildProject()) {
       return Collections.emptyList();
     }
 
-    List<RefRight> inherited = new ArrayList<RefRight>();
+    List<AccessSection> inherited = new ArrayList<AccessSection>();
     Set<Project.NameKey> seen = new HashSet<Project.NameKey>();
-    Project.NameKey parent = project.getParent();
+    Project.NameKey parent = getProject().getParent();
 
     while (parent != null && seen.add(parent)) {
       ProjectState s = projectCache.get(parent);
       if (s != null) {
-        inherited.addAll(s.getLocalRights());
+        inherited.addAll(s.getLocalAccessSections());
         parent = s.getProject().getParent();
       } else {
         break;
@@ -138,76 +151,21 @@
 
     // Wild project is the parent, or the root of the tree
     if (parent == null) {
-      inherited.addAll(getWildProjectRights());
-    }
-
-    return Collections.unmodifiableCollection(inherited);
-  }
-
-  private Collection<RefRight> getWildProjectRights() {
-    final ProjectState s = projectCache.get(wildProject);
-    return s != null ? s.getLocalRights() : Collections.<RefRight> emptyList();
-  }
-
-  /**
-   * Utility class that is needed to filter overridden refrights
-   */
-  private static class Grant {
-    final AccountGroup.Id group;
-    final String pattern;
-
-    private Grant(AccountGroup.Id group, String pattern) {
-      this.group = group;
-      this.pattern = pattern;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (o == null)
-        return false;
-      Grant grant = (Grant) o;
-      return group.equals(grant.group) && pattern.equals(grant.pattern);
-    }
-
-    @Override
-    public int hashCode() {
-      int result = group.hashCode();
-      result = 31 * result + pattern.hashCode();
-      return result;
-    }
-  }
-
-  /**
-   * Get the rights this project has and inherits from the wild project.
-   *
-   * @param action the category requested.
-   * @param dropOverridden whether to remove inherited permissions in case if we have a
-   *     local one that matches (action,group,ref)
-   * @return immutable collection of rights for the requested category.
-   */
-  public Collection<RefRight> getAllRights(ApprovalCategory.Id action, boolean dropOverridden) {
-    Collection<RefRight> rights = new LinkedList<RefRight>(getLocalRights(action));
-    rights.addAll(filter(getInheritedRights(), action));
-    if (dropOverridden) {
-      Set<Grant> grants = new HashSet<Grant>();
-      Iterator<RefRight> iter = rights.iterator();
-      while (iter.hasNext()) {
-        RefRight right = iter.next();
-
-        Grant grant = new Grant(right.getAccountGroupId(), right.getRefPattern());
-        if (grants.contains(grant)) {
-          iter.remove();
-        } else {
-          grants.add(grant);
-        }
+      ProjectState s = projectCache.get(wildProject);
+      if (s != null) {
+        inherited.addAll(s.getLocalAccessSections());
       }
     }
-    return Collections.unmodifiableCollection(rights);
+
+    return inherited;
   }
 
-  /** Is this the special wild project which manages inherited rights? */
-  public boolean isSpecialWildProject() {
-    return project.getNameKey().equals(wildProject);
+  /** Get both local and inherited access sections. */
+  public Collection<AccessSection> getAllAccessSections() {
+    List<AccessSection> all = new ArrayList<AccessSection>();
+    all.addAll(getLocalAccessSections());
+    all.addAll(getInheritedAccessSections());
+    return all;
   }
 
   /**
@@ -216,13 +174,13 @@
    *         are no local owners the local owners of the nearest parent project
    *         that has local owners are returned
    */
-  public Set<AccountGroup.Id> getOwners() {
-    if (!localOwners.isEmpty() || isSpecialWildProject()
-        || project.getParent() == null) {
+  public Set<AccountGroup.UUID> getOwners() {
+    Project.NameKey parentName = getProject().getParent();
+    if (!localOwners.isEmpty() || parentName == null || isWildProject()) {
       return localOwners;
     }
 
-    final ProjectState parent = projectCache.get(project.getParent());
+    ProjectState parent = projectCache.get(parentName);
     if (parent != null) {
       return parent.getOwners();
     }
@@ -237,13 +195,23 @@
    *         owners) and all groups to which the owner privilege for 'refs/*' is
    *         assigned for one of the parent projects (the inherited owners).
    */
-  public Set<AccountGroup.Id> getAllOwners() {
-    final HashSet<AccountGroup.Id> owners = new HashSet<AccountGroup.Id>();
-    for (final RefRight right : getAllRights(ApprovalCategory.OWN, true)) {
-      if (right.getMaxValue() > 0 && right.getRefPattern().equals(RefRight.ALL)) {
-        owners.add(right.getAccountGroupId());
+  public Set<AccountGroup.UUID> getAllOwners() {
+    HashSet<AccountGroup.UUID> owners = new HashSet<AccountGroup.UUID>();
+    owners.addAll(localOwners);
+
+    Set<Project.NameKey> seen = new HashSet<Project.NameKey>();
+    Project.NameKey parent = getProject().getParent();
+
+    while (parent != null && seen.add(parent)) {
+      ProjectState s = projectCache.get(parent);
+      if (s != null) {
+        owners.addAll(s.localOwners);
+        parent = s.getProject().getParent();
+      } else {
+        break;
       }
     }
+
     return Collections.unmodifiableSet(owners);
   }
 
@@ -255,20 +223,7 @@
     return projectControlFactory.create(user, this);
   }
 
-  private static Collection<RefRight> filter(Collection<RefRight> all,
-      ApprovalCategory.Id actionId) {
-    if (all.isEmpty()) {
-      return Collections.emptyList();
-    }
-    final Collection<RefRight> mine = new ArrayList<RefRight>(all.size());
-    for (final RefRight right : all) {
-      if (right.getApprovalCategoryId().equals(actionId)) {
-        mine.add(right);
-      }
-    }
-    if (mine.isEmpty()) {
-      return Collections.emptyList();
-    }
-    return Collections.unmodifiableCollection(mine);
+  private boolean isWildProject() {
+    return wildProject.equals(getProject().getNameKey());
   }
 }
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 8ddf585..ff1d09f 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,27 +14,16 @@
 
 package com.google.gerrit.server.project;
 
-import static com.google.gerrit.reviewdb.ApprovalCategory.FORGE_AUTHOR;
-import static com.google.gerrit.reviewdb.ApprovalCategory.FORGE_COMMITTER;
-import static com.google.gerrit.reviewdb.ApprovalCategory.FORGE_IDENTITY;
-import static com.google.gerrit.reviewdb.ApprovalCategory.FORGE_SERVER;
-import static com.google.gerrit.reviewdb.ApprovalCategory.OWN;
-import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD;
-import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD_CREATE;
-import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD_REPLACE;
-import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD_UPDATE;
-import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_TAG;
-import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_TAG_ANNOTATED;
-import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_TAG_SIGNED;
-import static com.google.gerrit.reviewdb.ApprovalCategory.READ;
-
+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.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.SystemConfig;
 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;
 
@@ -49,7 +38,6 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
@@ -57,8 +45,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.SortedMap;
-import java.util.TreeMap;
 import java.util.regex.Pattern;
 
 
@@ -68,18 +54,18 @@
     RefControl create(ProjectControl projectControl, String ref);
   }
 
-  private final SystemConfig systemConfig;
   private final ProjectControl projectControl;
   private final String refName;
 
+  private Map<String, List<PermissionRule>> permissions;
+
+  private Boolean owner;
   private Boolean canForgeAuthor;
   private Boolean canForgeCommitter;
 
   @Inject
-  protected RefControl(final SystemConfig systemConfig,
-      @Assisted final ProjectControl projectControl,
+  protected RefControl(@Assisted final ProjectControl projectControl,
       @Assisted String ref) {
-    this.systemConfig = systemConfig;
     if (isRE(ref)) {
       ref = shortestExample(ref);
 
@@ -114,26 +100,29 @@
 
   /** Is this user a ref owner? */
   public boolean isOwner() {
-    if (canPerform(OWN, (short) 1)) {
-      return true;
-    }
+    if (owner == null) {
+      if (canPerform(Permission.OWNER)) {
+        owner = true;
 
-    // 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.
-    //
-    if (getRefName().equals(
-        RefRight.ALL.substring(0, RefRight.ALL.length() - 1))) {
-      return getCurrentUser().isAdministrator();
-    } else {
-      return getProjectControl().isOwner();
+      } 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().isAdministrator();
+
+      } else {
+        owner = getProjectControl().isOwner();
+      }
     }
+    return owner;
   }
 
   /** Can this user see this reference exists? */
   public boolean isVisible() {
     return getProjectControl().visibleForReplication()
-        || canPerform(READ, (short) 1);
+        || canPerform(Permission.READ);
   }
 
   /**
@@ -144,27 +133,66 @@
    *         ref
    */
   public boolean canUpload() {
-    return canPerform(READ, (short) 2);
+    return getProjectControl()
+        .controlForRef("refs/for/" + getRefName())
+        .canPerform(Permission.PUSH);
   }
 
   /** @return true if this user can submit merge patch sets to this ref */
   public boolean canUploadMerges() {
-    return canPerform(READ, (short) 3);
+    return getProjectControl()
+      .controlForRef("refs/for/" + getRefName())
+      .canPerform(Permission.PUSH_MERGE);
   }
 
   /** @return true if this user can submit patch sets to this ref */
   public boolean canSubmit() {
-    return canPerform(ApprovalCategory.SUBMIT, (short) 1);
+    if (GitRepositoryManager.REF_CONFIG.equals(refName)) {
+      // Always allow project owners to submit configuration changes.
+      // Submitting configuration changes modifies the access control
+      // 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 canPerform(Permission.SUBMIT);
   }
 
   /** @return true if the user can update the reference as a fast-forward. */
   public boolean canUpdate() {
-    return canPerform(PUSH_HEAD, PUSH_HEAD_UPDATE);
+    if (GitRepositoryManager.REF_CONFIG.equals(refName)
+        && !getProjectControl().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
+      // a security hole enabling editing of access rules, and thus
+      // granting of powers beyond pushing to the configuration.
+      return false;
+    }
+    return canPerform(Permission.PUSH);
   }
 
   /** @return true if the user can rewind (force push) the reference. */
   public boolean canForceUpdate() {
-    return canPerform(PUSH_HEAD, PUSH_HEAD_REPLACE) || canDelete();
+    return canPushWithForce() || canDelete();
+  }
+
+  private boolean canPushWithForce() {
+    if (GitRepositoryManager.REF_CONFIG.equals(refName)
+        && !getProjectControl().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
+      // a security hole enabling editing of access rules, and thus
+      // granting of powers beyond pushing to the configuration.
+      return false;
+    }
+    for (PermissionRule rule : access(Permission.PUSH)) {
+      if (rule.getForce()) {
+        return true;
+      }
+    }
+    return false;
   }
 
   /**
@@ -186,7 +214,7 @@
     }
 
     if (object instanceof RevCommit) {
-      return owner || canPerform(PUSH_HEAD, PUSH_HEAD_CREATE);
+      return owner || canPerform(Permission.CREATE);
 
     } else if (object instanceof RevTag) {
       final RevTag tag = (RevTag) object;
@@ -208,7 +236,7 @@
         } else {
           valid = false;
         }
-        if (!valid && !owner && !canPerform(FORGE_IDENTITY, FORGE_COMMITTER)) {
+        if (!valid && !owner && !canForgeCommitter()) {
           return false;
         }
       }
@@ -217,9 +245,9 @@
       // than if it doesn't have a PGP signature.
       //
       if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) {
-        return owner || canPerform(PUSH_TAG, PUSH_TAG_SIGNED);
+        return owner || canPerform(Permission.PUSH_TAG);
       } else {
-        return owner || canPerform(PUSH_TAG, PUSH_TAG_ANNOTATED);
+        return owner || canPerform(Permission.PUSH_TAG);
       }
 
     } else {
@@ -234,12 +262,21 @@
    * @return {@code true} if the user specified can delete a Git ref.
    */
   public boolean canDelete() {
+    if (GitRepositoryManager.REF_CONFIG.equals(refName)) {
+      // Never allow removal of the refs/meta/config branch.
+      // Deleting the branch would destroy all Gerrit specific
+      // metadata about the project, including its access rules.
+      // If a project is to be removed from Gerrit, its repository
+      // should be removed first.
+      return false;
+    }
+
     switch (getCurrentUser().getAccessPath()) {
       case WEB_UI:
-        return isOwner() || canPerform(PUSH_HEAD, PUSH_HEAD_REPLACE);
+        return isOwner() || canPushWithForce();
 
       case GIT:
-        return canPerform(PUSH_HEAD, PUSH_HEAD_REPLACE);
+        return canPushWithForce();
 
       default:
         return false;
@@ -249,7 +286,7 @@
   /** @return true if this user can forge the author line in a commit. */
   public boolean canForgeAuthor() {
     if (canForgeAuthor == null) {
-      canForgeAuthor = canPerform(FORGE_IDENTITY, FORGE_AUTHOR);
+      canForgeAuthor = canPerform(Permission.FORGE_AUTHOR);
     }
     return canForgeAuthor;
   }
@@ -257,314 +294,103 @@
   /** @return true if this user can forge the committer line in a commit. */
   public boolean canForgeCommitter() {
     if (canForgeCommitter == null) {
-      canForgeCommitter = canPerform(FORGE_IDENTITY, FORGE_COMMITTER);
+      canForgeCommitter = canPerform(Permission.FORGE_COMMITTER);
     }
     return canForgeCommitter;
   }
 
   /** @return true if this user can forge the server on the committer line. */
   public boolean canForgeGerritServerIdentity() {
-    return canPerform(FORGE_IDENTITY, FORGE_SERVER);
+    return canPerform(Permission.FORGE_SERVER);
   }
 
-  public short normalize(ApprovalCategory.Id category, short score) {
-    short minAllowed = 0, maxAllowed = 0;
-    for (RefRight r : getApplicableRights(category)) {
-      if (getCurrentUser().getEffectiveGroups().contains(r.getAccountGroupId())) {
-        minAllowed = (short) Math.min(minAllowed, r.getMinValue());
-        maxAllowed = (short) Math.max(maxAllowed, r.getMaxValue());
+  /** 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()) {
+      if (Permission.isLabel(e.getKey())) {
+        r.add(toRange(e.getKey(), e.getValue()));
       }
     }
-
-    if (score < minAllowed) {
-      score = minAllowed;
-    }
-    if (score > maxAllowed) {
-      score = maxAllowed;
-    }
-    return score;
+    return r;
   }
 
-  /**
-   * Convenience holder class used to map a ref pattern to the list of
-   * {@code RefRight}s that use it in the database.
-   */
-  public final static class RefRightsForPattern {
-    private final List<RefRight> rights;
-    private boolean containsExclusive;
-
-    public RefRightsForPattern() {
-      rights = new ArrayList<RefRight>();
-      containsExclusive = false;
+  /** The range of permitted values associated with a label permission. */
+  public PermissionRange getRange(String permission) {
+    if (Permission.isLabel(permission)) {
+      return toRange(permission, access(permission));
     }
+    return null;
+  }
 
-    public void addRight(RefRight right) {
-      rights.add(right);
-      if (right.isExclusive()) {
-        containsExclusive = true;
-      }
+  private static PermissionRange toRange(String permissionName, List<PermissionRule> ruleList) {
+    int min = 0;
+    int max = 0;
+    for (PermissionRule rule : ruleList) {
+      min = Math.min(min, rule.getMin());
+      max = Math.max(max, rule.getMax());
     }
+    return new PermissionRange(permissionName, min, max);
+  }
 
-    public List<RefRight> getRights() {
-      return Collections.unmodifiableList(rights);
-    }
+  /** True if the user has this permission. Works only for non labels. */
+  boolean canPerform(String permissionName) {
+    return !access(permissionName).isEmpty();
+  }
 
-    public boolean containsExclusive() {
-      return containsExclusive;
-    }
+  /** 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();
+  }
 
-    /**
-     * Returns The max allowed value for this ref pattern for all specified
-     * groups.
-     *
-     * @param groups The groups of the user
-     * @return The allowed value for this ref for all the specified groups
-     */
-    private boolean allowedValueForRef(Set<AccountGroup.Id> groups, short level) {
-      for (RefRight right : rights) {
-        if (groups.contains(right.getAccountGroupId())
-            && right.getMaxValue() >= level) {
-          return true;
+  /** 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);
         }
       }
-      return false;
-    }
-  }
+      Collections.sort(sections, new MostSpecificComparator(getRefName()));
 
-  boolean canPerform(ApprovalCategory.Id actionId, short level) {
-    final Set<AccountGroup.Id> groups = getCurrentUser().getEffectiveGroups();
+      Set<SeenRule> seen = new HashSet<SeenRule>();
+      Set<String> exclusiveGroupPermissions = new HashSet<String>();
 
-    List<RefRight> allRights = new ArrayList<RefRight>();
-    allRights.addAll(getAllRights(actionId));
+      permissions = new HashMap<String, List<PermissionRule>>();
+      for (AccessSection section : sections) {
+        for (Permission permission : section.getPermissions()) {
+          if (exclusiveGroupPermissions.contains(permission.getName())) {
+            continue;
+          }
 
-    SortedMap<String, RefRightsForPattern> perPatternRights =
-      sortedRightsByPattern(allRights);
-
-    for (RefRightsForPattern right : perPatternRights.values()) {
-      if (right.allowedValueForRef(groups, level)) {
-        return true;
-      }
-      if (right.containsExclusive() && !actionId.equals(OWN)) {
-        break;
-      }
-    }
-    return false;
-  }
-
-  /**
-   * 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 final Comparator<String> BY_MOST_SPECIFIC_SORT =
-      new Comparator<String>() {
-        public 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;
+          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 (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(getRefName())) {
-            return 0;
-
-          } else {
-            return Math.max(pattern.length(), getRefName().length());
-          }
-          return StringUtils.getLevenshteinDistance(example, getRefName());
-        }
-
-        private boolean finite(String pattern) {
-          if (isRE(pattern)) {
-            return toRegExp(pattern).toAutomaton().isFinite();
-
-          } else if (pattern.endsWith("/*")) {
-            return false;
-
-          } else {
-            return true;
+          if (permission.getExclusiveGroup()) {
+            exclusiveGroupPermissions.add(permission.getName());
           }
         }
-
-        private int transitions(String pattern) {
-          if (isRE(pattern)) {
-            return toRegExp(pattern).toAutomaton().getNumberOfTransitions();
-
-          } else if (pattern.endsWith("/*")) {
-            return pattern.length();
-
-          } else {
-            return pattern.length();
-          }
-        }
-      };
-
-  /**
-   * Sorts all given rights into a map, ordered by descending length of
-   * ref pattern.
-   *
-   * For example, if given the following rights in argument:
-   *
-   * ["refs/heads/master", group1, -1, +1],
-   * ["refs/heads/master", group2, -2, +2],
-   * ["refs/heads/*", group3, -1, +1]
-   * ["refs/heads/stable", group2, -1, +1]
-   *
-   * Then the following map is returned:
-   * "refs/heads/master" => {
-   *      ["refs/heads/master", group1, -1, +1],
-   *      ["refs/heads/master", group2, -2, +2]
-   *  }
-   * "refs/heads/stable" => {["refs/heads/stable", group2, -1, +1]}
-   * "refs/heads/*" => {["refs/heads/*", group3, -1, +1]}
-   *
-   * @param actionRights
-   * @return A sorted map keyed off the ref pattern of all rights.
-   */
-  private SortedMap<String, RefRightsForPattern> sortedRightsByPattern(
-      List<RefRight> actionRights) {
-    SortedMap<String, RefRightsForPattern> rights =
-      new TreeMap<String, RefRightsForPattern>(BY_MOST_SPECIFIC_SORT);
-    for (RefRight actionRight : actionRights) {
-      RefRightsForPattern patternRights =
-        rights.get(actionRight.getRefPattern());
-      if (patternRights == null) {
-        patternRights = new RefRightsForPattern();
-        rights.put(actionRight.getRefPattern(), patternRights);
-      }
-      patternRights.addRight(actionRight);
-    }
-    return rights;
-  }
-
-  private List<RefRight> getAllRights(ApprovalCategory.Id actionId) {
-    final List<RefRight> allRefRights = filter(getProjectState().getAllRights(actionId, true));
-    return resolveOwnerGroups(allRefRights);
-  }
-
-  /**
-   * Returns all applicable rights for a given approval category.
-   *
-   * Applicable rights are defined as the list of {@code RefRight}s which match
-   * the ref for which this object was created, stopping the ref wildcard
-   * matching when an exclusive ref right was encountered, for the given
-   * approval category.
-   * @param id The {@link ApprovalCategory.Id}.
-   * @return All applicable rights.
-   */
-  public List<RefRight> getApplicableRights(final ApprovalCategory.Id id) {
-    List<RefRight> l = new ArrayList<RefRight>();
-    l.addAll(getAllRights(id));
-    SortedMap<String, RefRightsForPattern> perPatternRights =
-      sortedRightsByPattern(l);
-    List<RefRight> applicable = new ArrayList<RefRight>();
-    for (RefRightsForPattern patternRights : perPatternRights.values()) {
-      applicable.addAll(patternRights.getRights());
-      if (patternRights.containsExclusive()) {
-        break;
       }
     }
-    return Collections.unmodifiableList(applicable);
+    return permissions;
   }
 
-  /**
-   * Resolves all refRights which assign privileges to the 'Project Owners'
-   * group. All other refRights stay unchanged.
-   *
-   * @param refRights refRights to be resolved
-   * @return the resolved refRights
-   */
-  private List<RefRight> resolveOwnerGroups(final List<RefRight> refRights) {
-    final List<RefRight> resolvedRefRights =
-        new ArrayList<RefRight>(refRights.size());
-    for (final RefRight refRight : refRights) {
-      resolvedRefRights.addAll(resolveOwnerGroups(refRight));
-    }
-    return resolvedRefRights;
-  }
+  private boolean appliesToRef(AccessSection section) {
+    String refPattern = section.getRefPattern();
 
-  /**
-   * Checks if the given refRight assigns privileges to the 'Project Owners'
-   * group.
-   * If yes, resolves the 'Project Owners' group to the concrete groups that
-   * own the project and creates new refRights for the concrete owner groups
-   * which are returned.
-   * If no, the given refRight is returned unchanged.
-   *
-   * @param refRight refRight
-   * @return the resolved refRights
-   */
-  private Set<RefRight> resolveOwnerGroups(final RefRight refRight) {
-    final Set<RefRight> resolvedRefRights = new HashSet<RefRight>();
-    if (refRight.getAccountGroupId().equals(systemConfig.ownerGroupId)) {
-      for (final AccountGroup.Id ownerGroup : getProjectState().getAllOwners()) {
-        if (!ownerGroup.equals(systemConfig.ownerGroupId)) {
-          resolvedRefRights.add(new RefRight(refRight, ownerGroup));
-        }
-      }
-    } else {
-      resolvedRefRights.add(refRight);
-    }
-    return resolvedRefRights;
-  }
-
-  private List<RefRight> filter(Collection<RefRight> all) {
-    List<RefRight> mine = new ArrayList<RefRight>(all.size());
-    for (RefRight right : all) {
-      if (matches(right.getRefPattern())) {
-        mine.add(right);
-      }
-    }
-    return mine;
-  }
-
-  private ProjectState getProjectState() {
-    return projectControl.getProjectState();
-  }
-
-  private boolean matches(String refPattern) {
     if (isTemplate(refPattern)) {
       ParamertizedString template = new ParamertizedString(refPattern);
       HashMap<String, String> p = new HashMap<String, String>();
@@ -599,6 +425,18 @@
     }
   }
 
+  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);
+    }
+  }
+
   private static boolean isTemplate(String refPattern) {
     return 0 <= refPattern.indexOf("${");
   }
@@ -611,7 +449,7 @@
   }
 
   private static boolean isRE(String refPattern) {
-    return refPattern.startsWith(RefRight.REGEX_PREFIX);
+    return refPattern.startsWith(AccessSection.REGEX_PREFIX);
   }
 
   public static String shortestExample(String pattern) {
@@ -630,4 +468,143 @@
     }
     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.getRefPattern();
+      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.getRefPattern(), b.getRefPattern());
+    }
+
+    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/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 577c5af..074ad19 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.IntPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryBuilder;
@@ -107,6 +108,7 @@
     final Project.NameKey wildProjectName;
     final PatchListCache patchListCache;
     final GitRepositoryManager repoManager;
+    final ProjectCache projectCache;
 
     @Inject
     Arguments(Provider<ReviewDb> dbProvider,
@@ -118,7 +120,8 @@
         AuthConfig authConfig, ApprovalTypes approvalTypes,
         @WildProjectName Project.NameKey wildProjectName,
         PatchListCache patchListCache,
-        GitRepositoryManager repoManager) {
+        GitRepositoryManager repoManager,
+        ProjectCache projectCache) {
       this.dbProvider = dbProvider;
       this.rewriter = rewriter;
       this.userFactory = userFactory;
@@ -131,6 +134,7 @@
       this.wildProjectName = wildProjectName;
       this.patchListCache = patchListCache;
       this.repoManager = repoManager;
+      this.projectCache = projectCache;
     }
   }
 
@@ -340,15 +344,15 @@
     //
     AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(who));
     if (g != null) {
-      return visibleto(new SingleGroupUser(args.authConfig, g.getId()));
+      return visibleto(new SingleGroupUser(args.authConfig, g.getGroupUUID()));
     }
 
     Collection<AccountGroup> matches =
         args.groupCache.get(new AccountGroup.ExternalNameKey(who));
     if (matches != null && !matches.isEmpty()) {
-      HashSet<AccountGroup.Id> ids = new HashSet<AccountGroup.Id>();
+      HashSet<AccountGroup.UUID> ids = new HashSet<AccountGroup.UUID>();
       for (AccountGroup group : matches) {
-        ids.add(group.getId());
+        ids.add(group.getGroupUUID());
       }
       return visibleto(new SingleGroupUser(args.authConfig, ids));
     }
@@ -508,23 +512,19 @@
       // Try to match a project name by substring query.
       final List<ProjectPredicate> predicate =
           new ArrayList<ProjectPredicate>();
-      try {
-        for (final Project p : args.dbProvider.get().projects().all()) {
-          if (p.getName().toLowerCase().contains(query.toLowerCase())) {
-            predicate.add(new ProjectPredicate(args.dbProvider, p.getName()));
-          }
+      for (Project.NameKey name : args.projectCache.all()) {
+        if (name.get().toLowerCase().contains(query.toLowerCase())) {
+          predicate.add(new ProjectPredicate(args.dbProvider, name.get()));
         }
+      }
 
-        // If two or more projects contains "query" as substring create an
-        // OrPredicate holding predicates for all these projects, otherwise if
-        // only one contains that, return only that one predicate by itself.
-        if (predicate.size() == 1) {
-          return predicate.get(0);
-        } else if (predicate.size() > 1) {
-          return Predicate.or(predicate);
-        }
-      } catch (OrmException e) {
-        throw error("Cannot lookup project.", e);
+      // If two or more projects contains "query" as substring create an
+      // OrPredicate holding predicates for all these projects, otherwise if
+      // only one contains that, return only that one predicate by itself.
+      if (predicate.size() == 1) {
+        return predicate.get(0);
+      } else if (predicate.size() > 1) {
+        return Predicate.or(predicate);
       }
 
       throw error("Unsupported query:" + query);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
index 717b487..b31bf65 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
@@ -38,7 +38,8 @@
               new ChangeQueryBuilder.Arguments( //
                   new InvalidProvider<ReviewDb>(), //
                   new InvalidProvider<ChangeQueryRewriter>(), //
-                  null, null, null, null, null, null, null, null, null, null), null));
+                  null, null, null, null, null, null, null, //
+                  null, null, null, null), null));
 
   private final Provider<ReviewDb> dbProvider;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
index e76c278..ece61ef 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gerrit.reviewdb.ReviewDb;
@@ -33,48 +34,54 @@
   private static enum Test {
     EQ {
       @Override
-      public boolean match(short psValue, short expValue) {
+      public boolean match(int psValue, int expValue) {
         return psValue == expValue;
       }
     },
     GT_EQ {
       @Override
-      public boolean match(short psValue, short expValue) {
+      public boolean match(int psValue, int expValue) {
         return psValue >= expValue;
       }
     },
     LT_EQ {
       @Override
-      public boolean match(short psValue, short expValue) {
+      public boolean match(int psValue, int expValue) {
         return psValue <= expValue;
       }
     };
 
-    abstract boolean match(short psValue, short expValue);
+    abstract boolean match(int psValue, int expValue);
   }
 
-  private static ApprovalCategory.Id category(ApprovalTypes types, String toFind) {
-    if (types.getApprovalType(new ApprovalCategory.Id(toFind)) != null) {
-      return new ApprovalCategory.Id(toFind);
+  private static ApprovalCategory category(ApprovalTypes types, String toFind) {
+    if (types.byLabel(toFind) != null) {
+      return types.byLabel(toFind).getCategory();
+    }
+
+    if (types.byId(new ApprovalCategory.Id(toFind)) != null) {
+      return types.byId(new ApprovalCategory.Id(toFind)).getCategory();
     }
 
     for (ApprovalType at : types.getApprovalTypes()) {
-      String name = at.getCategory().getName();
-      if (toFind.equalsIgnoreCase(name)) {
-        return at.getCategory().getId();
+      ApprovalCategory category = at.getCategory();
 
-      } else if (toFind.equalsIgnoreCase(name.replace(" ", ""))) {
-        return at.getCategory().getId();
+      if (toFind.equalsIgnoreCase(category.getName())) {
+        return category;
+
+      } else if (toFind.equalsIgnoreCase(category.getName().replace(" ", ""))) {
+        return category;
       }
     }
 
     for (ApprovalType at : types.getApprovalTypes()) {
-      if (toFind.equalsIgnoreCase(at.getCategory().getAbbreviatedName())) {
-        return at.getCategory().getId();
+      ApprovalCategory category = at.getCategory();
+      if (toFind.equalsIgnoreCase(category.getAbbreviatedName())) {
+        return category;
       }
     }
 
-    return new ApprovalCategory.Id(toFind);
+    return new ApprovalCategory(new ApprovalCategory.Id(toFind), toFind);
   }
 
   private static Test op(String op) {
@@ -92,19 +99,20 @@
     }
   }
 
-  private static short value(String value) {
+  private static int value(String value) {
     if (value.startsWith("+")) {
       value = value.substring(1);
     }
-    return Short.parseShort(value);
+    return Integer.parseInt(value);
   }
 
   private final ChangeControl.GenericFactory ccFactory;
   private final IdentifiedUser.GenericFactory userFactory;
   private final Provider<ReviewDb> dbProvider;
   private final Test test;
-  private final ApprovalCategory.Id category;
-  private final short expVal;
+  private final ApprovalCategory category;
+  private final String permissionName;
+  private final int expVal;
 
   LabelPredicate(ChangeControl.GenericFactory ccFactory,
       IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
@@ -131,13 +139,15 @@
       test = Test.EQ;
       expVal = 1;
     }
+
+    this.permissionName = Permission.forLabel(category.getLabelName());
   }
 
   @Override
   public boolean match(final ChangeData object) throws OrmException {
     for (PatchSetApproval p : object.currentApprovals(dbProvider)) {
-      if (p.getCategoryId().equals(category)) {
-        short psVal = p.getValue();
+      if (p.getCategoryId().equals(category.getId())) {
+        int psVal = p.getValue();
         if (test.match(psVal, expVal)) {
           // Double check the value is still permitted for the user.
           //
@@ -149,7 +159,7 @@
               //
               continue;
             }
-            psVal = cc.normalize(category, psVal);
+            psVal = cc.getRange(permissionName).squash(psVal);
           } catch (NoSuchChangeException e) {
             // The project has disappeared.
             //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
index 2fb6694..1c37ac4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
@@ -26,19 +26,19 @@
 import java.util.Set;
 
 final class SingleGroupUser extends CurrentUser {
-  private final Set<AccountGroup.Id> groups;
+  private final Set<AccountGroup.UUID> groups;
 
-  SingleGroupUser(AuthConfig authConfig, AccountGroup.Id groupId) {
+  SingleGroupUser(AuthConfig authConfig, AccountGroup.UUID groupId) {
     this(authConfig, Collections.singleton(groupId));
   }
 
-  SingleGroupUser(AuthConfig authConfig, Set<AccountGroup.Id> groups) {
+  SingleGroupUser(AuthConfig authConfig, Set<AccountGroup.UUID> groups) {
     super(AccessPath.UNKNOWN, authConfig);
     this.groups = groups;
   }
 
   @Override
-  public Set<AccountGroup.Id> getEffectiveGroups() {
+  public Set<AccountGroup.UUID> getEffectiveGroups() {
     return groups;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java
index 55a36d6..6b66e33 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java
@@ -26,8 +26,6 @@
 public class DatabaseModule extends FactoryModule {
   @Override
   protected void configure() {
-    install(new SchemaVersion.Module());
-
     bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}).to(
         new TypeLiteral<Database<ReviewDb>>() {}).in(SINGLETON);
     bind(new TypeLiteral<Database<ReviewDb>>() {}).toProvider(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
index a24471a..9b6812f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -14,19 +14,26 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.gerrit.common.Version;
+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.gerrit.reviewdb.AccountGroupName;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.CurrentSchemaVersion;
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.SystemConfig;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.GroupUUID;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.workflow.NoOpFunction;
-import com.google.gerrit.server.workflow.SubmitFunction;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.NoReplication;
+import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.jdbc.JdbcExecutor;
@@ -37,6 +44,11 @@
 import com.google.gwtorm.schema.sql.SqlDialect;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -45,32 +57,45 @@
 /** Creates the current database schema and populates initial code rows. */
 public class SchemaCreator {
   private static final Project.NameKey DEFAULT_WILD_NAME =
-      new Project.NameKey("-- All Projects --");
+      new Project.NameKey("All-Projects");
 
   private final @SitePath
   File site_path;
 
+  private final GitRepositoryManager mgr;
+  private final PersonIdent serverUser;
+
   private final int versionNbr;
   private final ScriptRunner index_generic;
   private final ScriptRunner index_postgres;
   private final ScriptRunner mysql_nextval;
 
+  private AccountGroup admin;
+  private AccountGroup anonymous;
+  private AccountGroup registered;
+  private AccountGroup owners;
+
   @Inject
   public SchemaCreator(final SitePaths site,
-      @Current final SchemaVersion version) {
-    this(site.site_path, version);
+      @Current final SchemaVersion version, final GitRepositoryManager mgr,
+      @GerritPersonIdent final PersonIdent au) {
+    this(site.site_path, version, mgr, au);
   }
 
   public SchemaCreator(final @SitePath File site,
-      @Current final SchemaVersion version) {
+      @Current final SchemaVersion version, final GitRepositoryManager gitMgr,
+      final @GerritPersonIdent PersonIdent au) {
     site_path = site;
+    mgr = gitMgr;
+    serverUser = au;
     versionNbr = version.getVersionNbr();
     index_generic = new ScriptRunner("index_generic.sql");
     index_postgres = new ScriptRunner("index_postgres.sql");
     mysql_nextval = new ScriptRunner("mysql_nextval.sql");
   }
 
-  public void create(final ReviewDb db) throws OrmException {
+  public void create(final ReviewDb db) throws OrmException, IOException,
+      ConfigInvalidException {
     final JdbcSchema jdbc = (JdbcSchema) db;
     final JdbcExecutor e = new JdbcExecutor(jdbc);
     try {
@@ -84,15 +109,13 @@
     db.schemaVersion().insert(Collections.singleton(sVer));
 
     final SystemConfig sConfig = initSystemConfig(db);
-    initOwnerCategory(db);
-    initReadCategory(db, sConfig);
     initVerifiedCategory(db);
     initCodeReviewCategory(db, sConfig);
-    initSubmitCategory(db);
-    initPushTagCategory(db);
-    initPushUpdateBranchCategory(db);
-    initForgeIdentityCategory(db, sConfig);
-    initWildCardProject(db);
+
+    if (mgr != null) {
+      // TODO This should never be null when initializing a site.
+      initWildCardProject();
+    }
 
     final SqlDialect d = jdbc.getDialect();
     if (d instanceof DialectH2) {
@@ -110,19 +133,27 @@
     }
   }
 
+  private AccountGroup newGroup(ReviewDb c, String name, AccountGroup.UUID uuid)
+      throws OrmException {
+    if (uuid == null) {
+      uuid = GroupUUID.make(name, serverUser);
+    }
+    return new AccountGroup( //
+        new AccountGroup.NameKey(name), //
+        new AccountGroup.Id(c.nextAccountGroupId()), //
+        uuid);
+  }
+
   private SystemConfig initSystemConfig(final ReviewDb c) throws OrmException {
-    final AccountGroup admin =
-        new AccountGroup(new AccountGroup.NameKey("Administrators"),
-            new AccountGroup.Id(c.nextAccountGroupId()));
+    admin = newGroup(c, "Administrators", null);
     admin.setDescription("Gerrit Site Administrators");
     admin.setType(AccountGroup.Type.INTERNAL);
     c.accountGroups().insert(Collections.singleton(admin));
     c.accountGroupNames().insert(
         Collections.singleton(new AccountGroupName(admin)));
 
-    final AccountGroup anonymous =
-        new AccountGroup(new AccountGroup.NameKey("Anonymous Users"),
-            new AccountGroup.Id(c.nextAccountGroupId()));
+    anonymous =
+        newGroup(c, "Anonymous Users", AccountGroup.ANONYMOUS_USERS);
     anonymous.setDescription("Any user, signed-in or not");
     anonymous.setOwnerGroupId(admin.getId());
     anonymous.setType(AccountGroup.Type.SYSTEM);
@@ -130,9 +161,8 @@
     c.accountGroupNames().insert(
         Collections.singleton(new AccountGroupName(anonymous)));
 
-    final AccountGroup registered =
-        new AccountGroup(new AccountGroup.NameKey("Registered Users"),
-            new AccountGroup.Id(c.nextAccountGroupId()));
+    registered =
+        newGroup(c, "Registered Users", AccountGroup.REGISTERED_USERS);
     registered.setDescription("Any signed-in user");
     registered.setOwnerGroupId(admin.getId());
     registered.setType(AccountGroup.Type.SYSTEM);
@@ -140,9 +170,7 @@
     c.accountGroupNames().insert(
         Collections.singleton(new AccountGroupName(registered)));
 
-    final AccountGroup batchUsers =
-      new AccountGroup(new AccountGroup.NameKey("Non-Interactive Users"),
-          new AccountGroup.Id(c.nextAccountGroupId()));
+    final AccountGroup batchUsers = newGroup(c, "Non-Interactive Users", null);
     batchUsers.setDescription("Users who perform batch actions on Gerrit");
     batchUsers.setOwnerGroupId(admin.getId());
     batchUsers.setType(AccountGroup.Type.INTERNAL);
@@ -150,9 +178,7 @@
     c.accountGroupNames().insert(
         Collections.singleton(new AccountGroupName(batchUsers)));
 
-    final AccountGroup owners =
-        new AccountGroup(new AccountGroup.NameKey("Project Owners"),
-            new AccountGroup.Id(c.nextAccountGroupId()));
+    owners = newGroup(c, "Project Owners", AccountGroup.PROJECT_OWNERS);
     owners.setDescription("Any owner of the project");
     owners.setOwnerGroupId(admin.getId());
     owners.setType(AccountGroup.Type.SYSTEM);
@@ -162,10 +188,17 @@
 
     final SystemConfig s = SystemConfig.create();
     s.registerEmailPrivateKey = SignedToken.generateRandomKey();
+
     s.adminGroupId = admin.getId();
+    s.adminGroupUUID = admin.getGroupUUID();
+
     s.anonymousGroupId = anonymous.getId();
+
     s.registeredGroupId = registered.getId();
+
     s.batchUsersGroupId = batchUsers.getId();
+    s.batchUsersGroupUUID = batchUsers.getGroupUUID();
+
     s.ownerGroupId = owners.getId();
     s.wildProjectName = DEFAULT_WILD_NAME;
     try {
@@ -177,13 +210,64 @@
     return s;
   }
 
-  private void initWildCardProject(final ReviewDb c) throws OrmException {
-    final Project p;
+  private void initWildCardProject() throws IOException, ConfigInvalidException {
+    Repository git;
+    try {
+      git = mgr.openRepository(DEFAULT_WILD_NAME);
+    } catch (RepositoryNotFoundException notFound) {
+      // A repository may be missing if this project existed only to store
+      // inheritable permissions. For example 'All-Projects'.
+      try {
+        git = mgr.createRepository(DEFAULT_WILD_NAME);
+      } catch (RepositoryNotFoundException err) {
+        final String name = DEFAULT_WILD_NAME.get();
+        throw new IOException("Cannot create repository " + name, err);
+      }
+    }
+    try {
+      MetaDataUpdate md =
+          new MetaDataUpdate(new NoReplication(), DEFAULT_WILD_NAME, git);
+      md.getCommitBuilder().setAuthor(serverUser);
+      md.getCommitBuilder().setCommitter(serverUser);
 
-    p = new Project(DEFAULT_WILD_NAME);
-    p.setDescription("Rights inherited by all other projects");
-    p.setUseContributorAgreements(false);
-    c.projects().insert(Collections.singleton(p));
+      ProjectConfig config = ProjectConfig.read(md);
+      Project p = config.getProject();
+      p.setDescription("Rights inherited by all other projects");
+      p.setUseContributorAgreements(false);
+
+      AccessSection all = config.getAccessSection(AccessSection.ALL, true);
+      AccessSection heads = config.getAccessSection(AccessSection.HEADS, true);
+      AccessSection meta = config.getAccessSection(GitRepositoryManager.REF_CONFIG, true);
+
+      PermissionRule review = rule(config, registered);
+      review.setRange(-1, 1);
+      heads.getPermission(Permission.LABEL + "Code-Review", true).add(review);
+
+      all.getPermission(Permission.READ, true) //
+          .add(rule(config, admin));
+      all.getPermission(Permission.READ, true) //
+          .add(rule(config, anonymous));
+
+      config.getAccessSection("refs/for/" + AccessSection.ALL, true) //
+          .getPermission(Permission.PUSH, true) //
+          .add(rule(config, registered));
+      all.getPermission(Permission.FORGE_AUTHOR, true) //
+          .add(rule(config, registered));
+
+      meta.getPermission(Permission.READ, true) //
+          .add(rule(config, owners));
+
+      md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
+      if (!config.commit(md)) {
+        throw new IOException("Cannot create " + DEFAULT_WILD_NAME.get());
+      }
+    } finally {
+      git.close();
+    }
+  }
+
+  private PermissionRule rule(ProjectConfig config, AccountGroup group) {
+    return new PermissionRule(config.resolve(group));
   }
 
   private void initVerifiedCategory(final ReviewDb c) throws OrmException {
@@ -218,143 +302,6 @@
     vals.add(value(cat, -2, "Do not submit"));
     c.approvalCategories().insert(Collections.singleton(cat));
     c.approvalCategoryValues().insert(vals);
-
-    final RefRight approve =
-        new RefRight(new RefRight.Key(DEFAULT_WILD_NAME,
-            new RefRight.RefPattern("refs/heads/*"), cat.getId(),
-            sConfig.registeredGroupId));
-    approve.setMaxValue((short) 1);
-    approve.setMinValue((short) -1);
-    c.refRights().insert(Collections.singleton(approve));
-  }
-
-  private void initOwnerCategory(final ReviewDb c) throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> vals;
-
-    cat = new ApprovalCategory(ApprovalCategory.OWN, "Owner");
-    cat.setPosition((short) -1);
-    cat.setFunctionName(NoOpFunction.NAME);
-    vals = new ArrayList<ApprovalCategoryValue>();
-    vals.add(value(cat, 1, "Administer All Settings"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(vals);
-  }
-
-  private void initReadCategory(final ReviewDb c, final SystemConfig sConfig)
-      throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> vals;
-
-    cat = new ApprovalCategory(ApprovalCategory.READ, "Read Access");
-    cat.setPosition((short) -1);
-    cat.setFunctionName(NoOpFunction.NAME);
-    vals = new ArrayList<ApprovalCategoryValue>();
-    vals.add(value(cat, 3, "Upload merges permission"));
-    vals.add(value(cat, 2, "Upload permission"));
-    vals.add(value(cat, 1, "Read access"));
-    vals.add(value(cat, -1, "No access"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(vals);
-
-    final RefRight.RefPattern pattern = new RefRight.RefPattern(RefRight.ALL);
-    {
-      final RefRight read =
-          new RefRight(new RefRight.Key(DEFAULT_WILD_NAME, pattern,
-              cat.getId(), sConfig.anonymousGroupId));
-      read.setMaxValue((short) 1);
-      read.setMinValue((short) 1);
-      c.refRights().insert(Collections.singleton(read));
-    }
-    {
-      final RefRight read =
-          new RefRight(new RefRight.Key(DEFAULT_WILD_NAME, pattern,
-              cat.getId(), sConfig.registeredGroupId));
-      read.setMaxValue((short) 2);
-      read.setMinValue((short) 1);
-      c.refRights().insert(Collections.singleton(read));
-    }
-    {
-      final RefRight read =
-          new RefRight(new RefRight.Key(DEFAULT_WILD_NAME, pattern,
-              cat.getId(), sConfig.adminGroupId));
-      read.setMaxValue((short) 1);
-      read.setMinValue((short) 1);
-      c.refRights().insert(Collections.singleton(read));
-    }
-  }
-
-  private void initSubmitCategory(final ReviewDb c) throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> vals;
-
-    cat = new ApprovalCategory(ApprovalCategory.SUBMIT, "Submit");
-    cat.setPosition((short) -1);
-    cat.setFunctionName(SubmitFunction.NAME);
-    vals = new ArrayList<ApprovalCategoryValue>();
-    vals.add(value(cat, 1, "Submit"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(vals);
-  }
-
-  private void initPushTagCategory(final ReviewDb c) throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> vals;
-
-    cat = new ApprovalCategory(ApprovalCategory.PUSH_TAG, "Push Tag");
-    cat.setPosition((short) -1);
-    cat.setFunctionName(NoOpFunction.NAME);
-    vals = new ArrayList<ApprovalCategoryValue>();
-    vals.add(value(cat, ApprovalCategory.PUSH_TAG_SIGNED, "Create Signed Tag"));
-    vals.add(value(cat, ApprovalCategory.PUSH_TAG_ANNOTATED,
-        "Create Annotated Tag"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(vals);
-  }
-
-  private void initPushUpdateBranchCategory(final ReviewDb c)
-      throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> vals;
-
-    cat = new ApprovalCategory(ApprovalCategory.PUSH_HEAD, "Push Branch");
-    cat.setPosition((short) -1);
-    cat.setFunctionName(NoOpFunction.NAME);
-    vals = new ArrayList<ApprovalCategoryValue>();
-    vals.add(value(cat, ApprovalCategory.PUSH_HEAD_UPDATE, "Update Branch"));
-    vals.add(value(cat, ApprovalCategory.PUSH_HEAD_CREATE, "Create Branch"));
-    vals.add(value(cat, ApprovalCategory.PUSH_HEAD_REPLACE,
-        "Force Push Branch; Delete Branch"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(vals);
-  }
-
-  private void initForgeIdentityCategory(final ReviewDb c,
-      final SystemConfig sConfig) throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> values;
-
-    cat =
-        new ApprovalCategory(ApprovalCategory.FORGE_IDENTITY, "Forge Identity");
-    cat.setPosition((short) -1);
-    cat.setFunctionName(NoOpFunction.NAME);
-    values = new ArrayList<ApprovalCategoryValue>();
-    values.add(value(cat, ApprovalCategory.FORGE_AUTHOR,
-        "Forge Author Identity"));
-    values.add(value(cat, ApprovalCategory.FORGE_COMMITTER,
-        "Forge Committer or Tagger Identity"));
-    values.add(value(cat, ApprovalCategory.FORGE_SERVER,
-        "Forge Gerrit Code Review Server Identity"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(values);
-
-    RefRight right =
-        new RefRight(new RefRight.Key(sConfig.wildProjectName,
-            new RefRight.RefPattern(RefRight.ALL),
-            ApprovalCategory.FORGE_IDENTITY, sConfig.registeredGroupId));
-    right.setMinValue(ApprovalCategory.FORGE_AUTHOR);
-    right.setMaxValue(ApprovalCategory.FORGE_AUTHOR);
-    c.refRights().insert(Collections.singleton(right));
   }
 
   private static ApprovalCategoryValue value(final ApprovalCategory cat,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java
new file mode 100644
index 0000000..3225e13
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2009 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.schema;
+
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.GerritPersonIdentProvider;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+
+import org.eclipse.jgit.lib.PersonIdent;
+
+/** Validate the schema and connect to Git. */
+public class SchemaModule extends FactoryModule {
+  @Override
+  protected void configure() {
+    install(new SchemaVersion.Module());
+
+    bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class).toProvider(
+        GerritPersonIdentProvider.class);
+
+    bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class);
+    install(new LifecycleModule() {
+      @Override
+      protected void configure() {
+        listener().to(LocalDiskRepositoryManager.Lifecycle.class);
+      }
+    });
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
index f44eff5..3937aaa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
@@ -23,6 +23,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
 import java.io.IOException;
 import java.sql.SQLException;
 import java.util.Collections;
@@ -49,7 +51,13 @@
       final SchemaVersion u = updater.get();
       final CurrentSchemaVersion version = getSchemaVersion(db);
       if (version == null) {
-        creator.create(db);
+        try {
+          creator.create(db);
+        } catch (IOException e) {
+          throw new OrmException("Cannot initialize schema", e);
+        } catch (ConfigInvalidException e) {
+          throw new OrmException("Cannot initialize schema", e);
+        }
 
       } else {
         try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
index 978a152..5caa645 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  private static final Class<? extends SchemaVersion> C = Schema_52.class;
+  private static final Class<? extends SchemaVersion> C = Schema_55.class;
 
   public static class Module extends AbstractModule {
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_19.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_19.java
deleted file mode 100644
index 7ad91ff..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_19.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2009 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.schema;
-
-import com.google.gerrit.reviewdb.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-
-class Schema_19 extends SchemaVersion {
-  @Inject
-  Schema_19() {
-    super(new Provider<SchemaVersion>() {
-      public SchemaVersion get() {
-        throw new ProvisionException("Cannot upgrade from 18");
-      }
-    });
-  }
-
-  @Override
-  protected void upgradeFrom(UpdateUI ui, CurrentSchemaVersion curr,
-      ReviewDb db, boolean toTargetVersion) throws OrmException {
-    throw new OrmException("Cannot upgrade from " + curr.versionNbr
-        + "; manually run scripts from"
-        + " http://gerrit.googlecode.com/files/schema-upgrades003_019.zip"
-        + " and restart.");
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_20.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_20.java
deleted file mode 100644
index 4d8dca7..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_20.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2009 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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-class Schema_20 extends SchemaVersion {
-  @Inject
-  Schema_20(Provider<Schema_19> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_21.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_21.java
deleted file mode 100644
index ed50f7f..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_21.java
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (C) 2009 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.schema;
-
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectH2;
-import com.google.gwtorm.schema.sql.DialectMySQL;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Collections;
-
-class Schema_21 extends SchemaVersion {
-  @Inject
-  Schema_21(Provider<Schema_20> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    JdbcSchema jdbc = (JdbcSchema) db;
-    SystemConfig sc = db.systemConfig().get(new SystemConfig.Key());
-
-    Statement s = jdbc.getConnection().createStatement();
-    try {
-      ResultSet r;
-
-      r = s.executeQuery("SELECT name FROM projects WHERE project_id = 0");
-      try {
-        if (!r.next()) {
-          throw new OrmException("Cannot read old wild project");
-        }
-        sc.wildProjectName = new Project.NameKey(r.getString(1));
-      } finally {
-        r.close();
-      }
-
-      if (jdbc.getDialect() instanceof DialectMySQL) {
-        try {
-          s.execute("DROP FUNCTION nextval_project_id");
-        } catch (SQLException se) {
-          ui.message("warning: could not delete function nextval_project_id");
-        }
-
-      } else if (jdbc.getDialect() instanceof DialectH2) {
-        s.execute("ALTER TABLE projects DROP CONSTRAINT"
-            + " IF EXISTS CONSTRAINT_F3");
-      }
-    } finally {
-      s.close();
-    }
-
-    db.systemConfig().update(Collections.singleton(sc));
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_22.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_22.java
deleted file mode 100644
index 8e24aa2..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_22.java
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (C) 2009 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.schema;
-
-import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
-
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.AccountExternalId.Key;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectH2;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.Collection;
-
-class Schema_22 extends SchemaVersion {
-  @Inject
-  Schema_22(Provider<Schema_21> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    Statement s = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      ResultSet results =
-          s.executeQuery(//
-              "SELECT account_id, ssh_user_name"
-                  + " FROM accounts" //
-                  + " WHERE ssh_user_name IS NOT NULL"
-                  + " AND ssh_user_name <> ''");
-      Collection<AccountExternalId> ids = new ArrayList<AccountExternalId>();
-      while (results.next()) {
-        final int accountId = results.getInt(1);
-        final String userName = results.getString(2);
-
-        final Account.Id account = new Account.Id(accountId);
-        final AccountExternalId.Key key = toKey(userName);
-        ids.add(new AccountExternalId(account, key));
-      }
-      db.accountExternalIds().insert(ids);
-
-      if (((JdbcSchema) db).getDialect() instanceof DialectH2) {
-        s.execute("ALTER TABLE accounts DROP CONSTRAINT"
-            + " IF EXISTS CONSTRAINT_AF");
-      }
-    } finally {
-      s.close();
-    }
-  }
-
-  private Key toKey(final String userName) {
-    return new AccountExternalId.Key(SCHEME_USERNAME, userName);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_23.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_23.java
deleted file mode 100644
index 413fa4e..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_23.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (C) 2009 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.schema;
-
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.Collection;
-
-class Schema_23 extends SchemaVersion {
-  @Inject
-  Schema_23(Provider<Schema_22> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    Collection<AccountGroupName> names = new ArrayList<AccountGroupName>();
-    Statement queryStmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      ResultSet results =
-          queryStmt.executeQuery("SELECT group_id, name FROM account_groups");
-      while (results.next()) {
-        final int id = results.getInt(1);
-        final String name = results.getString(2);
-
-        final AccountGroup.Id group = new AccountGroup.Id(id);
-        final AccountGroup.NameKey key = toKey(name);
-        names.add(new AccountGroupName(key, group));
-      }
-    } finally {
-      queryStmt.close();
-    }
-    db.accountGroupNames().insert(names);
-  }
-
-  private AccountGroup.NameKey toKey(final String name) {
-    return new AccountGroup.NameKey(name);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_24.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_24.java
deleted file mode 100644
index 0172921..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_24.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-class Schema_24 extends SchemaVersion {
-  @Inject
-  Schema_24(Provider<Schema_23> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_25.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_25.java
deleted file mode 100644
index fdfff70..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_25.java
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.lib.Constants;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-class Schema_25 extends SchemaVersion {
-  private Set<ApprovalCategory.Id> nonActions;
-
-  @Inject
-  Schema_25(Provider<Schema_24> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    nonActions = new HashSet<ApprovalCategory.Id>();
-    for (ApprovalCategory c : db.approvalCategories().all()) {
-      if (!c.isAction()) {
-        nonActions.add(c.getId());
-      }
-    }
-
-    List<RefRight> rights = new ArrayList<RefRight>();
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      ResultSet rs = stmt.executeQuery("SELECT * FROM project_rights");
-      try {
-        while (rs.next()) {
-          rights.add(toRefRight(rs));
-        }
-      } finally {
-        rs.close();
-      }
-
-      db.refRights().insert(rights);
-      stmt.execute("CREATE INDEX ref_rights_byCatGroup"
-          + " ON ref_rights (category_id, group_id)");
-    } finally {
-      stmt.close();
-    }
-  }
-
-  private RefRight toRefRight(ResultSet rs) throws SQLException {
-    short min_value = rs.getShort("min_value");
-    short max_value = rs.getShort("max_value");
-    String category_id = rs.getString("category_id");
-    int group_id = rs.getInt("group_id");
-    String project_name = rs.getString("project_name");
-
-    ApprovalCategory.Id category = new ApprovalCategory.Id(category_id);
-    Project.NameKey project = new Project.NameKey(project_name);
-    AccountGroup.Id group = new AccountGroup.Id(group_id);
-
-    RefRight.RefPattern ref;
-    if (category.equals(ApprovalCategory.SUBMIT)
-        || category.equals(ApprovalCategory.PUSH_HEAD)
-        || nonActions.contains(category)) {
-      // Explicitly related to a branch head.
-      ref = new RefRight.RefPattern(Constants.R_HEADS + "*");
-
-    } else if (category.equals(ApprovalCategory.PUSH_TAG)) {
-      // Explicitly related to the tag namespace.
-      ref = new RefRight.RefPattern(Constants.R_TAGS + "/*");
-
-    } else if (category.equals(ApprovalCategory.READ)
-        || category.equals(ApprovalCategory.OWN)) {
-      // Currently these are project-wide rights, so apply that way.
-      ref = new RefRight.RefPattern(RefRight.ALL);
-
-    } else {
-      // Assume project wide for the default.
-      ref = new RefRight.RefPattern(RefRight.ALL);
-    }
-
-    RefRight.Key key = new RefRight.Key(project, ref, category, group);
-    RefRight r = new RefRight(key);
-    r.setMinValue(min_value);
-    r.setMaxValue(max_value);
-    return r;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_26.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_26.java
deleted file mode 100644
index 9c76af2..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_26.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectMySQL;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-class Schema_26 extends SchemaVersion {
-  @Inject
-  Schema_26(Provider<Schema_25> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    if (((JdbcSchema) db).getDialect() instanceof DialectMySQL) {
-      Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-      try {
-        stmt.execute("ALTER TABLE account_group_members_audit" //
-            + " MODIFY removed_on TIMESTAMP NULL DEFAULT NULL");
-        stmt.execute("UPDATE account_group_members_audit" //
-            + " SET removed_on = NULL" //
-            + " WHERE removed_by IS NULL;");
-      } finally {
-        stmt.close();
-      }
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_27.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_27.java
deleted file mode 100644
index f8febec..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_27.java
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectH2;
-import com.google.gwtorm.schema.sql.DialectMySQL;
-import com.google.gwtorm.schema.sql.DialectPostgreSQL;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-class Schema_27 extends SchemaVersion {
-  @Inject
-  Schema_27(Provider<Schema_26> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException, OrmException {
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      final SqlDialect dialect = ((JdbcSchema) db).getDialect();
-      if (dialect instanceof DialectPostgreSQL) {
-        stmt.execute("ALTER TABLE account_groups"
-            + " ALTER COLUMN name TYPE VARCHAR(255)");
-        stmt.execute("ALTER TABLE account_group_names"
-            + " ALTER COLUMN name TYPE VARCHAR(255)");
-
-      } else if (dialect instanceof DialectH2) {
-        stmt.execute("ALTER TABLE account_groups"
-            + " ALTER COLUMN name VARCHAR(255)");
-        stmt.execute("ALTER TABLE account_group_names"
-            + " ALTER COLUMN name VARCHAR(255) NOT NULL");
-
-      } else if (dialect instanceof DialectMySQL) {
-        stmt.execute("ALTER TABLE account_groups MODIFY name VARCHAR(255)");
-        stmt.execute("ALTER TABLE account_group_names"
-            + " MODIFY name VARCHAR(255)");
-
-      } else {
-        throw new OrmException("Unsupported dialect " + dialect);
-      }
-    } finally {
-      stmt.close();
-    }
-
-    // Some groups might be missing their names, our older schema
-    // creation logic failed to create the name objects. Do it now.
-    //
-    Map<AccountGroup.NameKey, AccountGroupName> names =
-        db.accountGroupNames().toMap(db.accountGroupNames().all());
-
-    List<AccountGroupName> insert = new ArrayList<AccountGroupName>();
-    List<AccountGroupName> update = new ArrayList<AccountGroupName>();
-
-    for (AccountGroup g : db.accountGroups().all()) {
-      AccountGroupName n = names.get(g.getNameKey());
-      if (n == null) {
-        insert.add(new AccountGroupName(g));
-
-      } else if (!g.getId().equals(n.getId())) {
-        n.setId(g.getId());
-        update.add(n);
-      }
-    }
-
-    db.accountGroupNames().insert(insert);
-    db.accountGroupNames().update(update);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_28.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_28.java
deleted file mode 100644
index cddbe4e..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_28.java
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.gerrit.server.workflow.NoOpFunction;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.Collections;
-
-class Schema_28 extends SchemaVersion {
-  @Inject
-  Schema_28(Provider<Schema_27> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    final SystemConfig cfg = db.systemConfig().get(new SystemConfig.Key());
-    ApprovalCategory cat;
-
-    initForgeIdentityCategory(db, cfg);
-
-    // Don't grant FORGE_COMMITTER to existing PUSH_HEAD rights. That
-    // is considered a bug that we are fixing with this schema upgrade.
-    // Administrators might need to relax permissions manually after the
-    // upgrade if that forgery is critical to their workflow.
-
-    cat = db.approvalCategories().get(ApprovalCategory.PUSH_TAG);
-    if (cat != null && "Push Annotated Tag".equals(cat.getName())) {
-      cat.setName("Push Tag");
-      db.approvalCategories().update(Collections.singleton(cat));
-    }
-
-    // Since we deleted Push Tags +3, drop anything using +3 down to +2.
-    //
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.execute("UPDATE ref_rights SET max_value = "
-          + ApprovalCategory.PUSH_TAG_ANNOTATED + " WHERE max_value >= 3");
-      stmt.execute("UPDATE ref_rights SET min_value = "
-          + ApprovalCategory.PUSH_TAG_ANNOTATED + " WHERE min_value >= 3");
-    } finally {
-      stmt.close();
-    }
-  }
-
-  private void initForgeIdentityCategory(final ReviewDb c,
-      final SystemConfig sConfig) throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> values;
-
-    cat =
-        new ApprovalCategory(ApprovalCategory.FORGE_IDENTITY, "Forge Identity");
-    cat.setPosition((short) -1);
-    cat.setFunctionName(NoOpFunction.NAME);
-    values = new ArrayList<ApprovalCategoryValue>();
-    values.add(value(cat, ApprovalCategory.FORGE_AUTHOR,
-        "Forge Author Identity"));
-    values.add(value(cat, ApprovalCategory.FORGE_COMMITTER,
-        "Forge Committer or Tagger Identity"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(values);
-
-    RefRight right =
-        new RefRight(new RefRight.Key(sConfig.wildProjectName,
-            new RefRight.RefPattern(RefRight.ALL),
-            ApprovalCategory.FORGE_IDENTITY, sConfig.registeredGroupId));
-    right.setMinValue(ApprovalCategory.FORGE_AUTHOR);
-    right.setMaxValue(ApprovalCategory.FORGE_AUTHOR);
-    c.refRights().insert(Collections.singleton(right));
-  }
-
-  private static ApprovalCategoryValue value(final ApprovalCategory cat,
-      final int value, final String name) {
-    return new ApprovalCategoryValue(new ApprovalCategoryValue.Id(cat.getId(),
-        (short) value), name);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_29.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_29.java
deleted file mode 100644
index 37920bf..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_29.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-class Schema_29 extends SchemaVersion {
-  @Inject
-  Schema_29(Provider<Schema_28> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_30.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_30.java
deleted file mode 100644
index 7285e32..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_30.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.util.Collections;
-
-class Schema_30 extends SchemaVersion {
-  @Inject
-  Schema_30(Provider<Schema_29> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    db.approvalCategoryValues().insert(
-        Collections.singleton(new ApprovalCategoryValue(
-            new ApprovalCategoryValue.Id(ApprovalCategory.FORGE_IDENTITY,
-                ApprovalCategory.FORGE_SERVER),
-            "Forge Gerrit Code Review Server Identity")));
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_31.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_31.java
deleted file mode 100644
index 62f57e2..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_31.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-class Schema_31 extends SchemaVersion {
-  @Inject
-  Schema_31(Provider<Schema_30> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.execute("CREATE INDEX changes_byProject"
-          + " ON changes (dest_project_name)");
-    } finally {
-      stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_32.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_32.java
deleted file mode 100644
index 2af5596..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_32.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_32 extends SchemaVersion {
-  @Inject
-  Schema_32(Provider<Schema_31> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_33.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_33.java
deleted file mode 100644
index 0c733d7..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_33.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.util.Collections;
-
-public class Schema_33 extends SchemaVersion {
-  @Inject
-  Schema_33(Provider<Schema_32> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    SystemConfig config = db.systemConfig().all().toList().get(0);
-    final AccountGroup batchUsers =
-      new AccountGroup(new AccountGroup.NameKey("Non-Interactive Users"),
-          new AccountGroup.Id(db.nextAccountGroupId()));
-    batchUsers.setDescription("Users who perform batch actions on Gerrit");
-    batchUsers.setOwnerGroupId(config.adminGroupId);
-    batchUsers.setType(AccountGroup.Type.INTERNAL);
-    db.accountGroups().insert(Collections.singleton(batchUsers));
-    db.accountGroupNames().insert(
-        Collections.singleton(new AccountGroupName(batchUsers)));
-
-    config.batchUsersGroupId = batchUsers.getId();
-    db.systemConfig().update(Collections.singleton(config));
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java
deleted file mode 100644
index fa94146..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java
+++ /dev/null
@@ -1,123 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.RefRight.RefPattern;
-import com.google.gerrit.server.project.RefControl.RefRightsForPattern;
-import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-public class Schema_34 extends SchemaVersion {
-  private static final Comparator<String> DESCENDING_SORT =
-      new Comparator<String>() {
-
-        @Override
-        public int compare(String a, String b) {
-          int aLength = a.length();
-          int bLength = b.length();
-          if (bLength == aLength) {
-            return a.compareTo(b);
-          }
-          return bLength - aLength;
-        }
-      };
-
-  @Inject
-  Schema_34(Provider<Schema_33> prior) {
-    super(prior);
-  }
-
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    Iterable<Project> projects = db.projects().all();
-    boolean showedBanner = false;
-
-    List<RefRight> toUpdate = new ArrayList<RefRight>();
-    List<RefRight> toDelete = new ArrayList<RefRight>();
-    for (Project p : projects) {
-      boolean showedProject = false;
-      List<RefRight> pr = db.refRights().byProject(p.getNameKey()).toList();
-      Map<ApprovalCategory.Id, Map<String, RefRightsForPattern>> r =
-        new HashMap<ApprovalCategory.Id, Map<String, RefRightsForPattern>>();
-      for (RefRight right : pr) {
-        ApprovalCategory.Id cat = right.getApprovalCategoryId();
-        if (r.get(cat) == null) {
-          Map<String, RefRightsForPattern> m =
-            new TreeMap<String, RefRightsForPattern>(DESCENDING_SORT);
-          r.put(cat, m);
-        }
-        if (r.get(cat).get(right.getRefPattern()) == null) {
-          RefRightsForPattern s = new RefRightsForPattern();
-          r.get(cat).put(right.getRefPattern(), s);
-        }
-        r.get(cat).get(right.getRefPattern()).addRight(right);
-      }
-
-      for (Map<String, RefRightsForPattern> categoryRights : r.values()) {
-        for (RefRightsForPattern rrp : categoryRights.values()) {
-          RefRight oldRight = rrp.getRights().get(0);
-          if (shouldPrompt(oldRight)) {
-            if (!showedBanner) {
-              ui.message("Entering interactive reference rights migration tool...");
-              showedBanner = true;
-            }
-            if (!showedProject) {
-              ui.message("In project " + p.getName());
-              showedProject = true;
-            }
-            ui.message("For category " + oldRight.getApprovalCategoryId());
-            boolean isWildcard = oldRight.getRefPattern().endsWith("/*");
-            boolean shouldUpdate = ui.yesno(!isWildcard,
-                "Should rights for pattern "
-                + oldRight.getRefPattern()
-                + " be considered exclusive?");
-            if (shouldUpdate) {
-              RefRight.Key newKey = new RefRight.Key(oldRight.getProjectNameKey(),
-                  new RefPattern("-" + oldRight.getRefPattern()),
-                  oldRight.getApprovalCategoryId(),
-                  oldRight.getAccountGroupId());
-              RefRight newRight = new RefRight(newKey);
-              newRight.setMaxValue(oldRight.getMaxValue());
-              newRight.setMinValue(oldRight.getMinValue());
-              toUpdate.add(newRight);
-              toDelete.add(oldRight);
-            }
-          }
-        }
-      }
-    }
-    db.refRights().insert(toUpdate);
-    db.refRights().delete(toDelete);
-  }
-
-  private boolean shouldPrompt(RefRight right) {
-    return !right.getRefPattern().equals("refs/*")
-      && !right.getRefPattern().equals("refs/heads/*")
-      && !right.getRefPattern().equals("refs/tags/*");
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_35.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_35.java
deleted file mode 100644
index 12d90c3..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_35.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public class Schema_35 extends SchemaVersion {
-  @Inject
-  Schema_35(Provider<Schema_34> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.execute("CREATE INDEX tracking_ids_byTrkId"
-          + " ON tracking_ids (tracking_id)");
-    } finally {
-      stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_36.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_36.java
deleted file mode 100644
index ba6b841..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_36.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectMySQL;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public class Schema_36 extends SchemaVersion {
-  @Inject
-  Schema_36(Provider<Schema_35> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      if (((JdbcSchema) db).getDialect() instanceof DialectMySQL) {
-        stmt.execute("DROP INDEX account_project_watches_ntNew ON account_project_watches");
-        stmt.execute("DROP INDEX account_project_watches_ntCmt ON account_project_watches");
-        stmt.execute("DROP INDEX account_project_watches_ntSub ON account_project_watches");
-      } else {
-        stmt.execute("DROP INDEX account_project_watches_ntNew");
-        stmt.execute("DROP INDEX account_project_watches_ntCmt");
-        stmt.execute("DROP INDEX account_project_watches_ntSub");
-      }
-      stmt.execute("CREATE INDEX account_project_watches_byProject"
-          + " ON account_project_watches (project_name)");
-    } finally {
-      stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_37.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_37.java
deleted file mode 100644
index 871f2e9..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_37.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_37 extends SchemaVersion {
-  @Inject
-  Schema_37(Provider<Schema_36> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_38.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_38.java
deleted file mode 100644
index 59d6fa2..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_38.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.List;
-
-public class Schema_38 extends SchemaVersion {
-  @Inject
-  Schema_38(Provider<Schema_37> prior) {
-    super(prior);
-  }
-
-  /**
-   * Migrate the account.default_context column to account_diff_preferences.context column.
-   * <p>
-   * Other fields in account_diff_preferences will be filled in with their defaults as
-   * defined in the {@link AccountDiffPreference#createDefault(com.google.gerrit.reviewdb.Account.Id)}
-   */
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
-      SQLException {
-    List<AccountDiffPreference> newPrefs =
-        new ArrayList<AccountDiffPreference>();
-
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      ResultSet result =
-          stmt.executeQuery("SELECT account_id, default_context"
-              + " FROM accounts WHERE default_context <> 10");
-      while (result.next()) {
-        int accountId = result.getInt(1);
-        short defaultContext = result.getShort(2);
-        AccountDiffPreference diffPref = AccountDiffPreference.createDefault(new Account.Id(accountId));
-        diffPref.setContext(defaultContext);
-        newPrefs.add(diffPref);
-      }
-    } finally {
-      stmt.close();
-    }
-
-    db.accountDiffPreferences().insert(newPrefs);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_39.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_39.java
deleted file mode 100644
index 39ae226..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_39.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_39 extends SchemaVersion {
-  @Inject
-  Schema_39(Provider<Schema_38> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_40.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_40.java
deleted file mode 100644
index 7d3e4f5..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_40.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectH2;
-import com.google.gwtorm.schema.sql.DialectMySQL;
-import com.google.gwtorm.schema.sql.DialectPostgreSQL;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public class Schema_40 extends SchemaVersion {
-  @Inject
-  Schema_40(Provider<Schema_39> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException,
-      OrmException {
-    // Set to "*" the filter field of the previously watched projects
-    //
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.execute("UPDATE account_project_watches" //
-          + " SET filter = '" + AccountProjectWatch.FILTER_ALL + "'" //
-          + " WHERE filter IS NULL OR filter = ''");
-
-      // Set the new primary key
-      //
-      final SqlDialect dialect = ((JdbcSchema) db).getDialect();
-      if (dialect instanceof DialectPostgreSQL) {
-        stmt.execute("ALTER TABLE account_project_watches "
-            + "DROP CONSTRAINT account_project_watches_pkey");
-        stmt.execute("ALTER TABLE account_project_watches "
-            + "ADD PRIMARY KEY (account_id, project_name, filter)");
-
-      } else if ((dialect instanceof DialectH2)
-          || (dialect instanceof DialectMySQL)) {
-        stmt.execute("ALTER TABLE account_project_watches DROP PRIMARY KEY");
-        stmt.execute("ALTER TABLE account_project_watches "
-            + "ADD PRIMARY KEY (account_id, project_name, filter)");
-
-      } else {
-        throw new OrmException("Unsupported dialect " + dialect);
-      }
-    } finally {
-      stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_41.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_41.java
deleted file mode 100644
index 508db43..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_41.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_41 extends SchemaVersion {
-  @Inject
-  Schema_41(Provider<Schema_40> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java
deleted file mode 100644
index 83bca7b..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_42 extends SchemaVersion {
-  @Inject
-  Schema_42(Provider<Schema_41> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_43.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_43.java
deleted file mode 100644
index 0edb7e5..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_43.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_43 extends SchemaVersion {
-  @Inject
-  Schema_43(Provider<Schema_42> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_44.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_44.java
deleted file mode 100644
index 4ab1986..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_44.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_44 extends SchemaVersion {
-  @Inject
-  Schema_44(Provider<Schema_43> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_45.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_45.java
deleted file mode 100644
index e37e87d..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_45.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_45 extends SchemaVersion {
-  @Inject
-  Schema_45(Provider<Schema_44> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_46.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_46.java
deleted file mode 100644
index e7b104c..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_46.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Collections;
-
-public class Schema_46 extends SchemaVersion {
-
-  @Inject
-  Schema_46(final Provider<Schema_45> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException,
-      OrmException {
-    AccountGroup.Id groupId = new AccountGroup.Id(db.nextAccountGroupId());
-
-    // update system_config
-    final Connection connection = ((JdbcSchema) db).getConnection();
-    Statement stmt = null;
-    try {
-      stmt = connection.createStatement();
-      stmt.execute("UPDATE system_config SET OWNER_GROUP_ID = " + groupId.get());
-      final ResultSet resultSet =
-          stmt.executeQuery("SELECT ADMIN_GROUP_ID FROM system_config");
-      resultSet.next();
-      final int adminGroupId = resultSet.getInt(1);
-
-      // create 'Project Owners' group
-      AccountGroup.NameKey nameKey = new AccountGroup.NameKey("Project Owners");
-      AccountGroup group = new AccountGroup(nameKey, groupId);
-      group.setType(AccountGroup.Type.SYSTEM);
-      group.setOwnerGroupId(new AccountGroup.Id(adminGroupId));
-      group.setDescription("Any owner of the project");
-      AccountGroupName gn = new AccountGroupName(group);
-      db.accountGroupNames().insert(Collections.singleton(gn));
-      db.accountGroups().insert(Collections.singleton(group));
-    } finally {
-      if (stmt != null) stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_47.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_47.java
deleted file mode 100644
index 124cc02..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_47.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_47 extends SchemaVersion {
-  @Inject
-  Schema_47(Provider<Schema_46> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_48.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_48.java
deleted file mode 100644
index 4e8b94d..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_48.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2010 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.schema;
-
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-import java.util.Collections;
-
-public class Schema_48 extends SchemaVersion {
-  @Inject
-  Schema_48(Provider<Schema_47> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    // Read +3 allows merges to be uploaded
-    db.approvalCategoryValues().insert(
-        Collections.singleton(new ApprovalCategoryValue(
-            new ApprovalCategoryValue.Id(ApprovalCategory.READ, (short) 3),
-            "Upload merges permission")));
-    // Since we added Read +3, elevate any Read +2 to that level to provide
-    // access equivalent to prior schema versions.
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.execute("UPDATE ref_rights SET max_value = 3"
-          + " WHERE category_id = '" + ApprovalCategory.READ.get()
-          + "' AND max_value = 2");
-    } finally {
-      stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
deleted file mode 100644
index 4a90c1f..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// 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.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_50 extends SchemaVersion {
-  @Inject
-  Schema_50(Provider<Schema_49> prior) {
-    super(prior);
-  }
-}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_51.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_51.java
deleted file mode 100644
index d111160..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_51.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// 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.schema;
-
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public class Schema_51 extends SchemaVersion {
-  @Inject
-  Schema_51(Provider<Schema_50> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.execute("CREATE INDEX account_group_includes_byInclude"
-          + " ON account_group_includes (include_id)");
-    } finally {
-      stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
index 3fbbbe0..e16b95c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
@@ -14,12 +14,28 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.gerrit.reviewdb.CurrentSchemaVersion;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
 
 public class Schema_52 extends SchemaVersion {
   @Inject
-  Schema_52(Provider<Schema_51> prior) {
-    super(prior);
+  Schema_52() {
+    super(new Provider<SchemaVersion>() {
+      public SchemaVersion get() {
+        throw new ProvisionException("Cannot upgrade from 51");
+      }
+    });
+  }
+
+  @Override
+  protected void upgradeFrom(UpdateUI ui, CurrentSchemaVersion curr,
+      ReviewDb db, boolean toTargetVersion) throws OrmException {
+    throw new OrmException("Cannot upgrade from schema " + curr.versionNbr
+        + "; manually run init from Gerrit Code Review 2.1.7"
+        + " and restart this version to continue.");
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
new file mode 100644
index 0000000..5e90e8e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
@@ -0,0 +1,473 @@
+// Copyright (C) 2010 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.schema;
+
+import static com.google.gerrit.common.data.Permission.CREATE;
+import static com.google.gerrit.common.data.Permission.FORGE_AUTHOR;
+import static com.google.gerrit.common.data.Permission.FORGE_COMMITTER;
+import static com.google.gerrit.common.data.Permission.FORGE_SERVER;
+import static com.google.gerrit.common.data.Permission.LABEL;
+import static com.google.gerrit.common.data.Permission.OWNER;
+import static com.google.gerrit.common.data.Permission.PUSH;
+import static com.google.gerrit.common.data.Permission.PUSH_MERGE;
+import static com.google.gerrit.common.data.Permission.PUSH_TAG;
+import static com.google.gerrit.common.data.Permission.READ;
+import static com.google.gerrit.common.data.Permission.SUBMIT;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.ApprovalCategory;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.SystemConfig;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.GroupUUID;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.NoReplication;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class Schema_53 extends SchemaVersion {
+  private final GitRepositoryManager mgr;
+  private final PersonIdent serverUser;
+
+  private SystemConfig systemConfig;
+  private Map<AccountGroup.Id, GroupReference> groupMap;
+  private Map<ApprovalCategory.Id, ApprovalCategory> categoryMap;
+  private GroupReference projectOwners;
+
+  private Map<Project.NameKey, Project.NameKey> parentsByProject;
+  private Map<Project.NameKey, List<OldRefRight>> rightsByProject;
+
+  private final String OLD_SUBMIT = "SUBM";
+  private final String OLD_READ = "READ";
+  private final String OLD_OWN = "OWN";
+  private final String OLD_PUSH_TAG = "pTAG";
+  private final String OLD_PUSH_HEAD = "pHD";
+  private final String OLD_FORGE_IDENTITY = "FORG";
+
+  @Inject
+  Schema_53(Provider<Schema_52> prior, GitRepositoryManager mgr,
+      @GerritPersonIdent PersonIdent serverUser) {
+    super(prior);
+    this.mgr = mgr;
+    this.serverUser = serverUser;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
+      SQLException {
+    systemConfig = db.systemConfig().get(new SystemConfig.Key());
+    categoryMap = db.approvalCategories().toMap(db.approvalCategories().all());
+
+    assignGroupUUIDs(db);
+    readOldRefRights(db);
+    readProjectParents(db);
+    exportProjectConfig(db);
+
+    deleteActionCategories(db);
+  }
+
+  private void deleteActionCategories(ReviewDb db) throws OrmException {
+    List<ApprovalCategory> delete = new ArrayList<ApprovalCategory>();
+    for (ApprovalCategory category : categoryMap.values()) {
+      if (category.getPosition() < 0) {
+        delete.add(category);
+      }
+    }
+    db.approvalCategories().delete(delete);
+  }
+
+  private void assignGroupUUIDs(ReviewDb db) throws OrmException {
+    groupMap = new HashMap<AccountGroup.Id, GroupReference>();
+    List<AccountGroup> groups = db.accountGroups().all().toList();
+    for (AccountGroup g : groups) {
+      if (g.getId().equals(systemConfig.ownerGroupId)) {
+        g.setGroupUUID(AccountGroup.PROJECT_OWNERS);
+        projectOwners = GroupReference.forGroup(g);
+
+      } else if (g.getId().equals(systemConfig.anonymousGroupId)) {
+        g.setGroupUUID(AccountGroup.ANONYMOUS_USERS);
+
+      } else if (g.getId().equals(systemConfig.registeredGroupId)) {
+        g.setGroupUUID(AccountGroup.REGISTERED_USERS);
+
+      } else {
+        g.setGroupUUID(GroupUUID.make(g.getName(), serverUser));
+      }
+      groupMap.put(g.getId(), GroupReference.forGroup(g));
+    }
+    db.accountGroups().update(groups);
+
+    systemConfig.adminGroupUUID = toUUID(systemConfig.adminGroupId);
+    systemConfig.batchUsersGroupUUID = toUUID(systemConfig.batchUsersGroupId);
+    db.systemConfig().update(Collections.singleton(systemConfig));
+  }
+
+  private AccountGroup.UUID toUUID(AccountGroup.Id id) {
+    return groupMap.get(id).getUUID();
+  }
+
+  private void exportProjectConfig(ReviewDb db) throws OrmException,
+      SQLException {
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM projects ORDER BY name");
+    while (rs.next()) {
+      final String name = rs.getString("name");
+      final Project.NameKey nameKey = new Project.NameKey(name);
+
+      Repository git;
+      try {
+        git = mgr.openRepository(nameKey);
+      } catch (RepositoryNotFoundException notFound) {
+        // A repository may be missing if this project existed only to store
+        // inheritable permissions. For example 'All-Projects'.
+        try {
+          git = mgr.createRepository(nameKey);
+        } catch (RepositoryNotFoundException err) {
+          throw new OrmException("Cannot create repository " + name, err);
+        }
+      }
+      try {
+        MetaDataUpdate md =
+            new MetaDataUpdate(new NoReplication(), nameKey, git);
+        md.getCommitBuilder().setAuthor(serverUser);
+        md.getCommitBuilder().setCommitter(serverUser);
+
+        ProjectConfig config = ProjectConfig.read(md);
+        loadProject(rs, config.getProject());
+        config.getAccessSections().clear();
+        convertRights(config);
+
+        // Grant out read on the config branch by default.
+        //
+        if (config.getProject().getNameKey().equals(systemConfig.wildProjectName)) {
+          AccessSection meta = config.getAccessSection(GitRepositoryManager.REF_CONFIG, true);
+          Permission read = meta.getPermission(READ, true);
+          read.getRule(config.resolve(projectOwners), true);
+        }
+
+        md.setMessage("Import project configuration from SQL\n");
+        if (!config.commit(md)) {
+          throw new OrmException("Cannot export project " + name);
+        }
+      } catch (ConfigInvalidException err) {
+        throw new OrmException("Cannot read project " + name, err);
+      } catch (IOException err) {
+        throw new OrmException("Cannot export project " + name, err);
+      } finally {
+        git.close();
+      }
+    }
+    rs.close();
+    stmt.close();
+  }
+
+  private void loadProject(ResultSet rs, Project project) throws SQLException,
+      OrmException {
+    project.setDescription(rs.getString("description"));
+    project.setUseContributorAgreements("Y".equals(rs
+        .getString("use_contributor_agreements")));
+
+    switch (rs.getString("submit_type").charAt(0)) {
+      case 'F':
+        project.setSubmitType(Project.SubmitType.FAST_FORWARD_ONLY);
+        break;
+      case 'M':
+        project.setSubmitType(Project.SubmitType.MERGE_IF_NECESSARY);
+        break;
+      case 'A':
+        project.setSubmitType(Project.SubmitType.MERGE_ALWAYS);
+        break;
+      case 'C':
+        project.setSubmitType(Project.SubmitType.CHERRY_PICK);
+        break;
+      default:
+        throw new OrmException("Unsupported submit_type="
+            + rs.getString("submit_type") + " on project " + project.getName());
+    }
+
+    project.setUseSignedOffBy("Y".equals(rs.getString("use_signed_off_by")));
+    project.setRequireChangeID("Y".equals(rs.getString("require_change_id")));
+    project.setUseContentMerge("Y".equals(rs.getString("use_content_merge")));
+    project.setParentName(rs.getString("parent_name"));
+  }
+
+  private void readOldRefRights(ReviewDb db) throws SQLException {
+    rightsByProject = new HashMap<Project.NameKey, List<OldRefRight>>();
+
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ref_rights");
+    while (rs.next()) {
+      OldRefRight right = new OldRefRight(rs);
+      if (right.group == null || right.category == null) {
+        continue;
+      }
+
+      List<OldRefRight> list;
+
+      list = rightsByProject.get(right.project);
+      if (list == null) {
+        list = new ArrayList<OldRefRight>();
+        rightsByProject.put(right.project, list);
+      }
+      list.add(right);
+    }
+    rs.close();
+    stmt.close();
+  }
+
+  private void readProjectParents(ReviewDb db) throws SQLException {
+    parentsByProject = new HashMap<Project.NameKey, Project.NameKey>();
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM projects");
+    while (rs.next()) {
+      String name = rs.getString("name");
+      String parent_name = rs.getString("parent_name");
+      if (parent_name == null) {
+        parent_name = systemConfig.wildProjectName.get();
+      }
+      parentsByProject.put(new Project.NameKey(name), //
+          new Project.NameKey(parent_name));
+    }
+    rs.close();
+    stmt.close();
+  }
+
+  private void convertRights(ProjectConfig config) {
+    List<OldRefRight> myRights =
+        rightsByProject.get(config.getProject().getNameKey());
+    if (myRights == null) {
+      return;
+    }
+
+    for (OldRefRight old : myRights) {
+      AccessSection section = config.getAccessSection(old.ref_pattern, true);
+      GroupReference group = config.resolve(old.group);
+
+      if (OLD_SUBMIT.equals(old.category)) {
+        PermissionRule submit = rule(group);
+        submit.setDeny(old.max_value <= 0);
+        add(section, SUBMIT, old.exclusive, submit);
+
+      } else if (OLD_READ.equals(old.category)) {
+        if (old.exclusive) {
+          section.getPermission(READ, true).setExclusiveGroup(true);
+          newChangePermission(config, old.ref_pattern).setExclusiveGroup(true);
+        }
+
+        PermissionRule read = rule(group);
+        read.setDeny(old.max_value <= 0);
+        add(section, READ, old.exclusive, read);
+
+        if (3 <= old.max_value) {
+          newMergePermission(config, old.ref_pattern).add(rule(group));
+        } else if (3 <= inheritedMax(config, old)) {
+          newMergePermission(config, old.ref_pattern).add(deny(group));
+        }
+
+        if (2 <= old.max_value) {
+          newChangePermission(config, old.ref_pattern).add(rule(group));
+        } else if (2 <= inheritedMax(config, old)) {
+          newChangePermission(config, old.ref_pattern).add(deny(group));
+        }
+
+      } else if (OLD_OWN.equals(old.category)) {
+        add(section, OWNER, false, rule(group));
+
+      } else if (OLD_PUSH_TAG.equals(old.category)) {
+        PermissionRule push = rule(group);
+        push.setDeny(old.max_value <= 0);
+        add(section, PUSH_TAG, old.exclusive, push);
+
+      } else if (OLD_PUSH_HEAD.equals(old.category)) {
+        if (old.exclusive) {
+          section.getPermission(PUSH, true).setExclusiveGroup(true);
+          section.getPermission(CREATE, true).setExclusiveGroup(true);
+        }
+
+        PermissionRule push = rule(group);
+        push.setDeny(old.max_value <= 0);
+        push.setForce(3 <= old.max_value);
+        add(section, PUSH, old.exclusive, push);
+
+        if (2 <= old.max_value) {
+          add(section, CREATE, old.exclusive, rule(group));
+        } else if (2 <= inheritedMax(config, old)) {
+          add(section, CREATE, old.exclusive, deny(group));
+        }
+
+      } else if (OLD_FORGE_IDENTITY.equals(old.category)) {
+        if (old.exclusive) {
+          section.getPermission(FORGE_AUTHOR, true).setExclusiveGroup(true);
+          section.getPermission(FORGE_COMMITTER, true).setExclusiveGroup(true);
+          section.getPermission(FORGE_SERVER, true).setExclusiveGroup(true);
+        }
+
+        if (1 <= old.max_value) {
+          add(section, FORGE_AUTHOR, old.exclusive, rule(group));
+        }
+
+        if (2 <= old.max_value) {
+          add(section, FORGE_COMMITTER, old.exclusive, rule(group));
+        } else if (2 <= inheritedMax(config, old)) {
+          add(section, FORGE_COMMITTER, old.exclusive, deny(group));
+        }
+
+        if (3 <= old.max_value) {
+          add(section, FORGE_SERVER, old.exclusive, rule(group));
+        } else if (3 <= inheritedMax(config, old)) {
+          add(section, FORGE_SERVER, old.exclusive, deny(group));
+        }
+
+      } else {
+        PermissionRule rule = rule(group);
+        rule.setRange(old.min_value, old.max_value);
+        if (old.min_value == 0 && old.max_value == 0) {
+          rule.setDeny(true);
+        }
+        add(section, LABEL + varNameOf(old.category), old.exclusive, rule);
+      }
+    }
+  }
+
+  private static Permission newChangePermission(ProjectConfig config,
+      String name) {
+    if (name.startsWith(AccessSection.REGEX_PREFIX)) {
+      name = AccessSection.REGEX_PREFIX
+          + "refs/for/"
+          + name.substring(AccessSection.REGEX_PREFIX.length());
+    } else {
+      name = "refs/for/" + name;
+    }
+    return config.getAccessSection(name, true).getPermission(PUSH, true);
+  }
+
+  private static Permission newMergePermission(ProjectConfig config,
+      String name) {
+    if (name.startsWith(AccessSection.REGEX_PREFIX)) {
+      name = AccessSection.REGEX_PREFIX
+          + "refs/for/"
+          + name.substring(AccessSection.REGEX_PREFIX.length());
+    } else {
+      name = "refs/for/" + name;
+    }
+    return config.getAccessSection(name, true).getPermission(PUSH_MERGE, true);
+  }
+
+  private static PermissionRule rule(GroupReference group) {
+    return new PermissionRule(group);
+  }
+
+  private static PermissionRule deny(GroupReference group) {
+    PermissionRule rule = rule(group);
+    rule.setDeny(true);
+    return rule;
+  }
+
+  private int inheritedMax(ProjectConfig config, OldRefRight old) {
+    int max = 0;
+
+    String ref = old.ref_pattern;
+    String category = old.category;
+    AccountGroup.UUID group = old.group.getUUID();
+
+    Project.NameKey project = config.getProject().getParent();
+    if (project == null) {
+      project = systemConfig.wildProjectName;
+    }
+    do {
+      List<OldRefRight> rights = rightsByProject.get(project);
+      if (rights != null) {
+        for (OldRefRight r : rights) {
+          if (r.ref_pattern.equals(ref) //
+              && r.group.getUUID().equals(group) //
+              && r.category.equals(category)) {
+            max = Math.max(max, r.max_value);
+            break;
+          }
+        }
+      }
+      project = parentsByProject.get(project);
+    } while (!project.equals(systemConfig.wildProjectName));
+
+    return max;
+  }
+
+  private String varNameOf(String id) {
+    ApprovalCategory category = categoryMap.get(new ApprovalCategory.Id(id));
+    if (category == null) {
+      category = new ApprovalCategory(new ApprovalCategory.Id(id), id);
+    }
+    return category.getLabelName();
+  }
+
+  private static void add(AccessSection section, String name,
+      boolean exclusive, PermissionRule rule) {
+    Permission p = section.getPermission(name, true);
+    if (exclusive) {
+      p.setExclusiveGroup(true);
+    }
+    p.add(rule);
+  }
+
+  private class OldRefRight {
+    final int min_value;
+    final int max_value;
+    final String ref_pattern;
+    final boolean exclusive;
+    final GroupReference group;
+    final String category;
+    final Project.NameKey project;
+
+    OldRefRight(ResultSet rs) throws SQLException {
+      min_value = rs.getInt("min_value");
+      max_value = rs.getInt("max_value");
+      project = new Project.NameKey(rs.getString("project_name"));
+
+      String r = rs.getString("ref_pattern");
+      exclusive = r.startsWith("-");
+      if (exclusive) {
+        r = r.substring(1);
+      }
+      ref_pattern = r;
+
+      category = rs.getString("category_id");
+      group = groupMap.get(new AccountGroup.Id(rs.getInt("group_id")));
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_54.java
similarity index 89%
rename from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_54.java
index 0977ee9..77c6775 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_54.java
@@ -17,10 +17,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_49 extends SchemaVersion {
-
+public class Schema_54 extends SchemaVersion {
   @Inject
-  Schema_49(Provider<Schema_48> prior) {
+  Schema_54(Provider<Schema_53> prior) {
     super(prior);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_55.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_55.java
new file mode 100644
index 0000000..8d00b73
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_55.java
@@ -0,0 +1,61 @@
+// 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.schema;
+
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.SystemConfig;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+import java.util.Collections;
+
+public class Schema_55 extends SchemaVersion {
+  private final LocalDiskRepositoryManager mgr;
+
+  @Inject
+  Schema_55(Provider<Schema_54> prior, LocalDiskRepositoryManager mgr) {
+    super(prior);
+    this.mgr = mgr;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
+    SystemConfig sc = db.systemConfig().get(new SystemConfig.Key());
+    String oldName = sc.wildProjectName.get();
+    String newName = "All-Projects";
+    if ("-- All Projects --".equals(oldName)) {
+      ui.message("Renaming \"" + oldName + "\" to \"" + newName + "\"");
+
+      File base = mgr.getBasePath();
+      File oldDir = FileKey.resolve(new File(base, oldName), FS.DETECTED);
+      File newDir = new File(base, newName + Constants.DOT_GIT_EXT);
+      if (!oldDir.renameTo(newDir)) {
+        throw new OrmException("Cannot rename " + oldDir.getAbsolutePath()
+            + " to " + newDir.getAbsolutePath());
+      }
+
+      sc.wildProjectName = new Project.NameKey(newName);
+      db.systemConfig().update(Collections.singleton(sc));
+    }
+  }
+}
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 6700393..ffed95a 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
@@ -15,11 +15,10 @@
 package com.google.gerrit.server.workflow;
 
 import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.RefRight;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.project.RefControl;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -29,7 +28,6 @@
   private static Map<String, CategoryFunction> all =
       new HashMap<String, CategoryFunction>();
   static {
-    all.put(SubmitFunction.NAME, new SubmitFunction());
     all.put(MaxWithBlock.NAME, new MaxWithBlock());
     all.put(MaxNoBlock.NAME, new MaxNoBlock());
     all.put(NoOpFunction.NAME, new NoOpFunction());
@@ -44,22 +42,11 @@
    *         is not known to Gerrit and thus cannot be executed.
    */
   public static CategoryFunction forCategory(final ApprovalCategory category) {
-    final CategoryFunction r = forName(category.getFunctionName());
+    final CategoryFunction r = all.get(category.getFunctionName());
     return r != null ? r : new NoOpFunction();
   }
 
   /**
-   * Locate a function by name.
-   *
-   * @param functionName the function's unique name.
-   * @return the function implementation; null if the function is not known to
-   *         Gerrit and thus cannot be executed.
-   */
-  public static CategoryFunction forName(final String functionName) {
-    return all.get(functionName);
-  }
-
-  /**
    * Normalize ChangeApprovals and set the valid flag for this category.
    * <p>
    * Implementors should invoke:
@@ -92,13 +79,8 @@
 
   public boolean isValid(final CurrentUser user, final ApprovalType at,
       final FunctionState state) {
-    RefControl rc = state.controlFor(user);
-    for (final RefRight pr : rc.getApplicableRights(at.getCategory().getId())) {
-      if (user.getEffectiveGroups().contains(pr.getAccountGroupId())
-          && (pr.getMinValue() < 0 || pr.getMaxValue() > 0)) {
-        return true;
-      }
-    }
-    return false;
+    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 36a52e2..2cb3e81 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
@@ -16,13 +16,13 @@
 
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.RefRight;
 import com.google.gerrit.reviewdb.ApprovalCategory.Id;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -139,28 +139,12 @@
    * of them is used.
    * <p>
    */
-  private void applyRightFloor(final PatchSetApproval a) {
+  private void applyRightFloor(final ApprovalType at, final PatchSetApproval a) {
+    final ApprovalCategory category = at.getCategory();
+    final String permission = Permission.forLabel(category.getLabelName());
     final IdentifiedUser user = userFactory.create(a.getAccountId());
-    RefControl rc = controlFor(user);
-
-    // Find the maximal range actually granted to the user.
-    //
-    short minAllowed = 0, maxAllowed = 0;
-    for (final RefRight r : rc.getApplicableRights(a.getCategoryId())) {
-      final AccountGroup.Id grp = r.getAccountGroupId();
-      if (user.getEffectiveGroups().contains(grp)) {
-        minAllowed = (short) Math.min(minAllowed, r.getMinValue());
-        maxAllowed = (short) Math.max(maxAllowed, r.getMaxValue());
-      }
-    }
-
-    // Normalize the value into that range.
-    //
-    if (a.getValue() < minAllowed) {
-      a.setValue(minAllowed);
-    } else if (a.getValue() > maxAllowed) {
-      a.setValue(maxAllowed);
-    }
+    final PermissionRange range = controlFor(user).getRange(permission);
+    a.setValue((short) range.squash(a.getValue()));
   }
 
   RefControl controlFor(final CurrentUser user) {
@@ -172,7 +156,7 @@
   /** Run <code>applyTypeFloor</code>, <code>applyRightFloor</code>. */
   public void normalize(final ApprovalType at, final PatchSetApproval ca) {
     applyTypeFloor(at, ca);
-    applyRightFloor(ca);
+    applyRightFloor(at, ca);
   }
 
   private static Id id(final ApprovalType at) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/SubmitFunction.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/SubmitFunction.java
deleted file mode 100644
index f0a00ff..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/SubmitFunction.java
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (C) 2008 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.workflow;
-
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.project.RefControl;
-
-/**
- * Computes if the submit function can be used.
- * <p>
- * In order to be considered "approved" this function requires that all approval
- * categories with a position >= 0 (that is any whose
- * {@link ApprovalCategory#isAction()} method returns false) is valid and that
- * the change state be {@link Change.Status#NEW}.
- * <p>
- * This is mostly useful for actions, like {@link ApprovalCategory#SUBMIT}.
- */
-public class SubmitFunction extends CategoryFunction {
-  public static String NAME = "Submit";
-
-  @Override
-  public void run(final ApprovalType at, final FunctionState state) {
-    state.valid(at, valid(at, state));
-  }
-
-  @Override
-  public boolean isValid(final CurrentUser user, final ApprovalType at,
-      final FunctionState state) {
-    if (valid(at, state)) {
-      RefControl rc = state.controlFor(user);
-      for (final RefRight pr : rc.getApplicableRights(at.getCategory().getId())) {
-        if (user.getEffectiveGroups().contains(pr.getAccountGroupId())
-            && pr.getMaxValue() > 0) {
-          return true;
-        }
-      }
-    }
-    return false;
-  }
-
-  private static boolean valid(final ApprovalType at, final FunctionState state) {
-    if (state.getChange().getStatus() != Change.Status.NEW) {
-      return false;
-    }
-    for (final ApprovalType t : state.getApprovalTypes()) {
-      if (!state.isValid(t)) {
-        return false;
-      }
-    }
-    return true;
-  }
-}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
new file mode 100644
index 0000000..e8a235f
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -0,0 +1,191 @@
+// 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.Project;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class ProjectConfigTest extends LocalDiskRepositoryTestCase {
+  private final GroupReference developers = new GroupReference(
+      new AccountGroup.UUID("X"), "Developers");
+  private final GroupReference staff = new GroupReference(
+      new AccountGroup.UUID("Y"), "Staff");
+
+  private Repository db;
+  private TestRepository<Repository> util;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    db = createBareRepository();
+    util = new TestRepository<Repository>(db);
+  }
+
+  @Test
+  public void testReadConfig() throws Exception {
+    RevCommit rev = util.commit(util.tree( //
+        util.file("groups", util.blob(group(developers))), //
+        util.file("project.config", util.blob(""//
+            + "[access \"refs/heads/*\"]\n" //
+            + "  exclusiveGroupPermissions = read submit create\n" //
+            + "  submit = group Developers\n" //
+            + "  push = group Developers\n" //
+            + "  read = group Developers\n")) //
+        ));
+
+    ProjectConfig cfg = read(rev);
+    AccessSection section = cfg.getAccessSection("refs/heads/*");
+    assertNotNull("has refs/heads/*", section);
+    assertNull("no refs/*", cfg.getAccessSection("refs/*"));
+
+    Permission create = section.getPermission(Permission.CREATE);
+    Permission submit = section.getPermission(Permission.SUBMIT);
+    Permission read = section.getPermission(Permission.READ);
+    Permission push = section.getPermission(Permission.PUSH);
+
+    assertTrue(create.getExclusiveGroup());
+    assertTrue(submit.getExclusiveGroup());
+    assertTrue(read.getExclusiveGroup());
+    assertFalse(push.getExclusiveGroup());
+  }
+
+  @Test
+  public void testEditConfig() throws Exception {
+    RevCommit rev = util.commit(util.tree( //
+        util.file("groups", util.blob(group(developers))), //
+        util.file("project.config", util.blob(""//
+            + "[access \"refs/heads/*\"]\n" //
+            + "  exclusiveGroupPermissions = read submit\n" //
+            + "  submit = group Developers\n" //
+            + "  upload = group Developers\n" //
+            + "  read = group Developers\n")) //
+        ));
+    update(rev);
+
+    ProjectConfig cfg = read(rev);
+    AccessSection section = cfg.getAccessSection("refs/heads/*");
+    Permission submit = section.getPermission(Permission.SUBMIT);
+    submit.add(new PermissionRule(cfg.resolve(staff)));
+    rev = commit(cfg);
+    assertEquals(""//
+        + "[access \"refs/heads/*\"]\n" //
+        + "  exclusiveGroupPermissions = read submit\n" //
+        + "  submit = group Developers\n" //
+        + "\tsubmit = group Staff\n" //
+        + "  upload = group Developers\n" //
+        + "  read = group Developers\n", text(rev, "project.config"));
+  }
+
+  @Test
+  public void testEditConfigMissingGroupTableEntry() throws Exception {
+    RevCommit rev = util.commit(util.tree( //
+        util.file("groups", util.blob(group(developers))), //
+        util.file("project.config", util.blob(""//
+            + "[access \"refs/heads/*\"]\n" //
+            + "  exclusiveGroupPermissions = read submit\n" //
+            + "  submit = group People Who Can Submit\n" //
+            + "  upload = group Developers\n" //
+            + "  read = group Developers\n")) //
+        ));
+    update(rev);
+
+    ProjectConfig cfg = read(rev);
+    AccessSection section = cfg.getAccessSection("refs/heads/*");
+    Permission submit = section.getPermission(Permission.SUBMIT);
+    submit.add(new PermissionRule(cfg.resolve(staff)));
+    rev = commit(cfg);
+    assertEquals(""//
+        + "[access \"refs/heads/*\"]\n" //
+        + "  exclusiveGroupPermissions = read submit\n" //
+        + "  submit = group People Who Can Submit\n" //
+        + "\tsubmit = group Staff\n" //
+        + "  upload = group Developers\n" //
+        + "  read = group Developers\n", text(rev, "project.config"));
+  }
+
+  private ProjectConfig read(RevCommit rev) throws IOException,
+      ConfigInvalidException {
+    ProjectConfig cfg = new ProjectConfig(new Project.NameKey("test"));
+    cfg.load(db, rev);
+    return cfg;
+  }
+
+  private RevCommit commit(ProjectConfig cfg) throws IOException,
+      MissingObjectException, IncorrectObjectTypeException {
+    MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), //
+        cfg.getProject().getNameKey(), //
+        db);
+    util.tick(5);
+    util.setAuthorAndCommitter(md.getCommitBuilder());
+    md.setMessage("Edit\n");
+    assertTrue("commit finished", cfg.commit(md));
+
+    Ref ref = db.getRef(GitRepositoryManager.REF_CONFIG);
+    return util.getRevWalk().parseCommit(ref.getObjectId());
+  }
+
+  private void update(RevCommit rev) throws Exception {
+    RefUpdate u = db.updateRef(GitRepositoryManager.REF_CONFIG);
+    u.disableRefLog();
+    u.setNewObjectId(rev);
+    switch (u.forceUpdate()) {
+      case FAST_FORWARD:
+      case FORCED:
+      case NEW:
+      case NO_CHANGE:
+        break;
+      default:
+        fail("Cannot update ref for test: " + u.getResult());
+    }
+  }
+
+  private String text(RevCommit rev, String path) throws Exception {
+    RevObject blob = util.get(rev.getTree(), path);
+    byte[] data = db.open(blob).getCachedBytes(Integer.MAX_VALUE);
+    return RawParseUtils.decode(data);
+  }
+
+  private static String group(GroupReference g) {
+    return g.getUUID().get() + "\t" + g.getName() + "\n";
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
index f819dac..6d56b4a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
@@ -278,7 +278,7 @@
     account.setFullName(name);
     account.setPreferredEmail(email);
     final AccountState s =
-        new AccountState(account, Collections.<AccountGroup.Id> emptySet(),
+        new AccountState(account, Collections.<AccountGroup.UUID> emptySet(),
             Collections.<AccountExternalId> emptySet());
     return s;
   }
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 6b0f13a..80fa477 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
@@ -14,23 +14,25 @@
 
 package com.google.gerrit.server.project;
 
-import static com.google.gerrit.reviewdb.ApprovalCategory.OWN;
-import static com.google.gerrit.reviewdb.ApprovalCategory.READ;
-import static com.google.gerrit.reviewdb.ApprovalCategory.SUBMIT;
+import static com.google.gerrit.common.data.Permission.OWNER;
+import static com.google.gerrit.common.data.Permission.PUSH;
+import static com.google.gerrit.common.data.Permission.READ;
+import static com.google.gerrit.common.data.Permission.SUBMIT;
 
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
 import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.gerrit.reviewdb.RefRight.RefPattern;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ProjectConfig;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
@@ -41,17 +43,17 @@
 import org.eclipse.jgit.lib.Config;
 
 import java.io.UnsupportedEncodingException;
-import java.util.ArrayList;
 import java.util.Arrays;
 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;
 
 public class RefControlTest extends TestCase {
   public void testOwnerProject() {
-    grant(local, OWN, admin, "refs/*", 1);
+    grant(local, OWNER, admin, "refs/*");
 
     ProjectControl uBlah = user(devs);
     ProjectControl uAdmin = user(devs, admin);
@@ -61,8 +63,8 @@
   }
 
   public void testBranchDelegation1() {
-    grant(local, OWN, admin, "refs/*", 1);
-    grant(local, OWN, devs, "refs/heads/x/*", 1);
+    grant(local, OWNER, admin, "refs/*");
+    grant(local, OWNER, devs, "refs/heads/x/*");
 
     ProjectControl uDev = user(devs);
     assertFalse("not owner", uDev.isOwner());
@@ -77,9 +79,10 @@
   }
 
   public void testBranchDelegation2() {
-    grant(local, OWN, admin, "refs/*", 1);
-    grant(local, OWN, devs, "refs/heads/x/*", 1);
-    grant(local, OWN, fixers, "-refs/heads/x/y/*", 1);
+    grant(local, OWNER, admin, "refs/*");
+    grant(local, OWNER, devs, "refs/heads/x/*");
+    grant(local, OWNER, fixers, "refs/heads/x/y/*");
+    doNotInherit(local, OWNER, "refs/heads/x/y/*");
 
     ProjectControl uDev = user(devs);
     assertFalse("not owner", uDev.isOwner());
@@ -104,8 +107,11 @@
   }
 
   public void testInheritRead_SingleBranchDeniesUpload() {
-    grant(parent, READ, registered, "refs/*", 1, 2);
-    grant(local, READ, registered, "-refs/heads/foobar", 1);
+    grant(parent, READ, registered, "refs/*");
+    grant(parent, PUSH, registered, "refs/for/refs/*");
+    grant(local, READ, registered, "refs/heads/foobar");
+    doNotInherit(local, READ, "refs/heads/foobar");
+    doNotInherit(local, PUSH, "refs/for/refs/heads/foobar");
 
     ProjectControl u = user();
     assertTrue("can upload", u.canPushToAtLeastOneRef());
@@ -118,8 +124,9 @@
   }
 
   public void testInheritRead_SingleBranchDoesNotOverrideInherited() {
-    grant(parent, READ, registered, "refs/*", 1, 2);
-    grant(local, READ, registered, "refs/heads/foobar", 1);
+    grant(parent, READ, registered, "refs/*");
+    grant(parent, PUSH, registered, "refs/for/refs/*");
+    grant(local, READ, registered, "refs/heads/foobar");
 
     ProjectControl u = user();
     assertTrue("can upload", u.canPushToAtLeastOneRef());
@@ -132,16 +139,16 @@
   }
 
   public void testInheritRead_OverrideWithDeny() {
-    grant(parent, READ, registered, "refs/*", 1);
-    grant(local, READ, registered, "refs/*", 0);
+    grant(parent, READ, registered, "refs/*");
+    grant(local, READ, registered, "refs/*").setDeny(true);
 
     ProjectControl u = user();
     assertFalse("can't read", u.isVisible());
   }
 
   public void testInheritRead_AppendWithDenyOfRef() {
-    grant(parent, READ, registered, "refs/*", 1);
-    grant(local, READ, registered, "refs/heads/*", 0);
+    grant(parent, READ, registered, "refs/*");
+    grant(local, READ, registered, "refs/heads/*").setDeny(true);
 
     ProjectControl u = user();
     assertTrue("can read", u.isVisible());
@@ -151,9 +158,9 @@
   }
 
   public void testInheritRead_OverridesAndDeniesOfRef() {
-    grant(parent, READ, registered, "refs/*", 1);
-    grant(local, READ, registered, "refs/*", 0);
-    grant(local, READ, registered, "refs/heads/*", -1, 1);
+    grant(parent, READ, registered, "refs/*");
+    grant(local, READ, registered, "refs/*").setDeny(true);
+    grant(local, READ, registered, "refs/heads/*");
 
     ProjectControl u = user();
     assertTrue("can read", u.isVisible());
@@ -163,9 +170,9 @@
   }
 
   public void testInheritSubmit_OverridesAndDeniesOfRef() {
-    grant(parent, SUBMIT, registered, "refs/*", 1);
-    grant(local, SUBMIT, registered, "refs/*", 0);
-    grant(local, SUBMIT, registered, "refs/heads/*", -1, 1);
+    grant(parent, SUBMIT, registered, "refs/*");
+    grant(local, SUBMIT, registered, "refs/*").setDeny(true);
+    grant(local, SUBMIT, registered, "refs/heads/*");
 
     ProjectControl u = user();
     assertFalse("can't submit", u.controlForRef("refs/foobar").canSubmit());
@@ -174,8 +181,9 @@
   }
 
   public void testCannotUploadToAnyRef() {
-    grant(parent, READ, registered, "refs/*", 1);
-    grant(local, READ, devs, "refs/heads/*", 1, 2);
+    grant(parent, READ, registered, "refs/*");
+    grant(local, READ, devs, "refs/heads/*");
+    grant(local, PUSH, devs, "refs/for/refs/heads/*");
 
     ProjectControl u = user();
     assertFalse("cannot upload", u.canPushToAtLeastOneRef());
@@ -186,15 +194,14 @@
 
   // -----------------------------------------------------------------------
 
-  private final Project.NameKey local = new Project.NameKey("test");
-  private final Project.NameKey parent = new Project.NameKey("parent");
-  private final AccountGroup.Id admin = new AccountGroup.Id(1);
-  private final AccountGroup.Id anonymous = new AccountGroup.Id(2);
-  private final AccountGroup.Id registered = new AccountGroup.Id(3);
-  private final AccountGroup.Id owners = new AccountGroup.Id(4);
+  private ProjectConfig local;
+  private ProjectConfig parent;
+  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;
 
-  private final AccountGroup.Id devs = new AccountGroup.Id(5);
-  private final AccountGroup.Id fixers = new AccountGroup.Id(6);
+  private final AccountGroup.UUID devs = new AccountGroup.UUID("test.devs");
+  private final AccountGroup.UUID fixers = new AccountGroup.UUID("test.fixers");
 
   private final SystemConfig systemConfig;
   private final AuthConfig authConfig;
@@ -202,11 +209,8 @@
 
   public RefControlTest() {
     systemConfig = SystemConfig.create();
-    systemConfig.adminGroupId = admin;
-    systemConfig.anonymousGroupId = anonymous;
-    systemConfig.registeredGroupId = registered;
-    systemConfig.ownerGroupId = owners;
-    systemConfig.batchUsersGroupId = anonymous;
+    systemConfig.adminGroupUUID = admin;
+    systemConfig.batchUsersGroupUUID = anonymous;
     try {
       byte[] bin = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8");
       systemConfig.registerEmailPrivateKey = Base64.encodeBase64String(bin);
@@ -230,14 +234,16 @@
     anonymousUser = injector.getInstance(AnonymousUser.class);
   }
 
-  private List<RefRight> localRights;
-  private List<RefRight> inheritedRights;
-
   @Override
-  protected void setUp() throws Exception {
+  public void setUp() throws Exception {
     super.setUp();
-    localRights = new ArrayList<RefRight>();
-    inheritedRights = new ArrayList<RefRight>();
+
+    parent = new ProjectConfig(new Project.NameKey("parent"));
+    parent.createInMemory();
+
+    local = new ProjectConfig(new Project.NameKey("local"));
+    local.createInMemory();
+    local.getProject().setParentName(parent.getProject().getName());
   }
 
   private static void assertOwner(String ref, ProjectControl u) {
@@ -248,64 +254,91 @@
     assertFalse("NOT OWN " + ref, u.controlForRef(ref).isOwner());
   }
 
-  private void grant(Project.NameKey project, ApprovalCategory.Id categoryId,
-      AccountGroup.Id group, String ref, int maxValue) {
-    grant(project, categoryId, group, ref, maxValue, maxValue);
+  private PermissionRule grant(ProjectConfig project, String permissionName,
+      AccountGroup.UUID group, String ref) {
+    PermissionRule rule = newRule(project, group);
+    project.getAccessSection(ref, true) //
+        .getPermission(permissionName, true) //
+        .add(rule);
+    return rule;
   }
 
-  private void grant(Project.NameKey project, ApprovalCategory.Id categoryId, AccountGroup.Id group,
-      String ref, int minValue, int maxValue) {
-    RefRight right =
-        new RefRight(new RefRight.Key(project, new RefPattern(ref),
-            categoryId, group));
-    right.setMinValue((short) minValue);
-    right.setMaxValue((short) maxValue);
-
-    if (project == parent) {
-      inheritedRights.add(right);
-    } else if (project == local) {
-      localRights.add(right);
-    } else {
-      fail("Unknown project key: " + project);
-    }
+  private void doNotInherit(ProjectConfig project, String permissionName,
+      String ref) {
+    project.getAccessSection(ref, true) //
+        .getPermission(permissionName, true) //
+        .setExclusiveGroup(true);
   }
 
-  private ProjectControl user(AccountGroup.Id... memberOf) {
+  private PermissionRule newRule(ProjectConfig project, AccountGroup.UUID groupUUID) {
+    GroupReference group = new GroupReference(groupUUID, groupUUID.get());
+    group = project.resolve(group);
+
+    return new PermissionRule(group);
+  }
+
+  private ProjectControl user(AccountGroup.UUID... memberOf) {
     RefControl.Factory refControlFactory = new RefControl.Factory() {
       @Override
       public RefControl create(final ProjectControl projectControl, final String ref) {
-        return new RefControl(systemConfig, projectControl, ref);
+        return new RefControl(projectControl, ref);
       }
     };
-    return new ProjectControl(systemConfig,
-        Collections.<AccountGroup.Id> emptySet(),
-        Collections.<AccountGroup.Id> emptySet(), refControlFactory,
+    return new ProjectControl(Collections.<AccountGroup.UUID> emptySet(),
+        Collections.<AccountGroup.UUID> emptySet(), refControlFactory,
         new MockUser(memberOf), newProjectState());
   }
 
   private ProjectState newProjectState() {
-    ProjectCache projectCache = null;
-    Project.NameKey wildProject = new Project.NameKey("-- All Projects --");
+    final Map<Project.NameKey, ProjectState> all =
+        new HashMap<Project.NameKey, ProjectState>();
+    final ProjectCache projectCache = new ProjectCache() {
+      @Override
+      public ProjectState get(Project.NameKey projectName) {
+        return all.get(projectName);
+      }
+
+      @Override
+      public void evict(Project p) {
+      }
+
+      @Override
+      public Iterable<Project.NameKey> all() {
+        return Collections.emptySet();
+      }
+
+      @Override
+      public Iterable<Project.NameKey> byName(String prefix) {
+        return Collections.emptySet();
+      }
+
+      @Override
+      public void onCreateProject(Project.NameKey newProjectName) {
+      }
+    };
+
+    GitRepositoryManager mgr = null;
+    Project.NameKey wildProject = new Project.NameKey("All-Projects");
     ProjectControl.AssistedFactory projectControlFactory = null;
-    ProjectState ps =
-        new ProjectState(anonymousUser, projectCache, wildProject,
-            projectControlFactory, new Project(parent), localRights);
-    ps.setInheritedRights(inheritedRights);
-    return ps;
+    all.put(local.getProject().getNameKey(), new ProjectState(anonymousUser,
+        projectCache, wildProject, projectControlFactory, mgr, local));
+    all.put(parent.getProject().getNameKey(), new ProjectState(anonymousUser,
+        projectCache, wildProject, projectControlFactory, mgr, parent));
+    return all.get(local.getProject().getNameKey());
   }
 
   private class MockUser extends CurrentUser {
-    private final Set<AccountGroup.Id> groups;
+    private final Set<AccountGroup.UUID> groups;
 
-    MockUser(AccountGroup.Id[] groupId) {
+    MockUser(AccountGroup.UUID[] groupId) {
       super(AccessPath.UNKNOWN, RefControlTest.this.authConfig);
-      groups = new HashSet<AccountGroup.Id>(Arrays.asList(groupId));
+      groups = new HashSet<AccountGroup.UUID>(Arrays.asList(groupId));
       groups.add(registered);
       groups.add(anonymous);
     }
 
     @Override
-    public Set<AccountGroup.Id> getEffectiveGroups() {
+    public Set<AccountGroup.UUID> getEffectiveGroups() {
       return groups;
     }
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
index ee9af13..1d70d4e 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
@@ -17,12 +17,8 @@
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.gerrit.server.workflow.NoOpFunction;
-import com.google.gerrit.server.workflow.SubmitFunction;
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.jdbc.JdbcSchema;
@@ -151,24 +147,6 @@
     }
   }
 
-  public void testCreateSchema_WildCardProject() throws OrmException {
-    final ReviewDb c = db.create().open();
-    try {
-      final SystemConfig cfg;
-      final Project all;
-
-      cfg = c.systemConfig().get(new SystemConfig.Key());
-      all = c.projects().get(cfg.wildProjectName);
-      assertNotNull(all);
-      assertEquals("-- All Projects --", all.getName());
-      assertFalse(all.isUseContributorAgreements());
-      assertFalse(all.isUseSignedOffBy());
-      assertFalse(all.isRequireChangeID());
-    } finally {
-      c.close();
-    }
-  }
-
   public void testCreateSchema_ApprovalCategory_CodeReview()
       throws OrmException {
     final ReviewDb c = db.create().open();
@@ -182,7 +160,6 @@
       assertEquals("R", cat.getAbbreviatedName());
       assertEquals("MaxWithBlock", cat.getFunctionName());
       assertTrue(cat.isCopyMinScore());
-      assertFalse(cat.isAction());
       assertTrue(0 <= cat.getPosition());
     } finally {
       c.close();
@@ -190,101 +167,6 @@
     assertValueRange(codeReview, -2, -1, 0, 1, 2);
   }
 
-  public void testCreateSchema_ApprovalCategory_Read() throws OrmException {
-    final ReviewDb c = db.create().open();
-    try {
-      final ApprovalCategory cat;
-
-      cat = c.approvalCategories().get(ApprovalCategory.READ);
-      assertNotNull(cat);
-      assertEquals(ApprovalCategory.READ, cat.getId());
-      assertEquals("Read Access", cat.getName());
-      assertNull(cat.getAbbreviatedName());
-      assertEquals(NoOpFunction.NAME, cat.getFunctionName());
-      assertTrue(cat.isAction());
-    } finally {
-      c.close();
-    }
-    assertValueRange(ApprovalCategory.READ, -1, 1, 2, 3);
-  }
-
-  public void testCreateSchema_ApprovalCategory_Submit() throws OrmException {
-    final ReviewDb c = db.create().open();
-    try {
-      final ApprovalCategory cat;
-
-      cat = c.approvalCategories().get(ApprovalCategory.SUBMIT);
-      assertNotNull(cat);
-      assertEquals(ApprovalCategory.SUBMIT, cat.getId());
-      assertEquals("Submit", cat.getName());
-      assertNull(cat.getAbbreviatedName());
-      assertEquals(SubmitFunction.NAME, cat.getFunctionName());
-      assertTrue(cat.isAction());
-    } finally {
-      c.close();
-    }
-    assertValueRange(ApprovalCategory.SUBMIT, 1);
-  }
-
-  public void testCreateSchema_ApprovalCategory_PushTag() throws OrmException {
-    final ReviewDb c = db.create().open();
-    try {
-      final ApprovalCategory cat;
-
-      cat = c.approvalCategories().get(ApprovalCategory.PUSH_TAG);
-      assertNotNull(cat);
-      assertEquals(ApprovalCategory.PUSH_TAG, cat.getId());
-      assertEquals("Push Tag", cat.getName());
-      assertNull(cat.getAbbreviatedName());
-      assertEquals(NoOpFunction.NAME, cat.getFunctionName());
-      assertTrue(cat.isAction());
-    } finally {
-      c.close();
-    }
-    assertValueRange(ApprovalCategory.PUSH_TAG, //
-        ApprovalCategory.PUSH_TAG_SIGNED, //
-        ApprovalCategory.PUSH_TAG_ANNOTATED);
-  }
-
-  public void testCreateSchema_ApprovalCategory_PushHead() throws OrmException {
-    final ReviewDb c = db.create().open();
-    try {
-      final ApprovalCategory cat;
-
-      cat = c.approvalCategories().get(ApprovalCategory.PUSH_HEAD);
-      assertNotNull(cat);
-      assertEquals(ApprovalCategory.PUSH_HEAD, cat.getId());
-      assertEquals("Push Branch", cat.getName());
-      assertNull(cat.getAbbreviatedName());
-      assertEquals(NoOpFunction.NAME, cat.getFunctionName());
-      assertTrue(cat.isAction());
-    } finally {
-      c.close();
-    }
-    assertValueRange(ApprovalCategory.PUSH_HEAD, //
-        ApprovalCategory.PUSH_HEAD_UPDATE, //
-        ApprovalCategory.PUSH_HEAD_CREATE, //
-        ApprovalCategory.PUSH_HEAD_REPLACE);
-  }
-
-  public void testCreateSchema_ApprovalCategory_Owner() throws OrmException {
-    final ReviewDb c = db.create().open();
-    try {
-      final ApprovalCategory cat;
-
-      cat = c.approvalCategories().get(ApprovalCategory.OWN);
-      assertNotNull(cat);
-      assertEquals(ApprovalCategory.OWN, cat.getId());
-      assertEquals("Owner", cat.getName());
-      assertNull(cat.getAbbreviatedName());
-      assertEquals(NoOpFunction.NAME, cat.getFunctionName());
-      assertTrue(cat.isAction());
-    } finally {
-      c.close();
-    }
-    assertValueRange(ApprovalCategory.OWN, 1);
-  }
-
   private void assertValueRange(ApprovalCategory.Id cat, int... range)
       throws OrmException {
     final HashSet<ApprovalCategoryValue.Id> act =
@@ -314,57 +196,4 @@
       fail("Category " + cat + " has additional values: " + act);
     }
   }
-
-  public void testCreateSchema_DefaultAccess_AnonymousUsers()
-      throws OrmException {
-    db.create();
-    final SystemConfig config = db.getSystemConfig();
-    assertDefaultRight("refs/*", config.anonymousGroupId,
-        ApprovalCategory.READ, 1, 1);
-  }
-
-  public void testCreateSchema_DefaultAccess_RegisteredUsers()
-      throws OrmException {
-    db.create();
-    final SystemConfig config = db.getSystemConfig();
-    assertDefaultRight("refs/*", config.registeredGroupId,
-        ApprovalCategory.READ, 1, 2);
-    assertDefaultRight("refs/heads/*", config.registeredGroupId, codeReview,
-        -1, 1);
-  }
-
-  public void testCreateSchema_DefaultAccess_Administrators()
-      throws OrmException {
-    db.create();
-    final SystemConfig config = db.getSystemConfig();
-    assertDefaultRight("refs/*", config.adminGroupId, ApprovalCategory.READ, 1,
-        1);
-  }
-
-  private void assertDefaultRight(final String pattern,
-      final AccountGroup.Id group, final ApprovalCategory.Id category, int min,
-      int max) throws OrmException {
-    final ReviewDb c = db.open();
-    try {
-      final SystemConfig cfg;
-      final Project all;
-      final RefRight right;
-
-      cfg = c.systemConfig().get(new SystemConfig.Key());
-      all = c.projects().get(cfg.wildProjectName);
-      right =
-          c.refRights().get(
-              new RefRight.Key(all.getNameKey(), new RefRight.RefPattern(
-                  pattern), category, group));
-
-      assertNotNull(right);
-      assertEquals(all.getNameKey(), right.getProjectNameKey());
-      assertEquals(group, right.getAccountGroupId());
-      assertEquals(category, right.getApprovalCategoryId());
-      assertEquals(min, right.getMinValue());
-      assertEquals(max, right.getMaxValue());
-    } finally {
-      c.close();
-    }
-  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
index a009f24..f2ba70e 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
@@ -16,7 +16,12 @@
 
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.SystemConfig;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.GerritPersonIdentProvider;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.SchemaFactory;
@@ -27,6 +32,9 @@
 
 import junit.framework.TestCase;
 
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.util.List;
@@ -58,6 +66,22 @@
         bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}).toInstance(db);
         bind(SitePaths.class).toInstance(paths);
         install(new SchemaVersion.Module());
+
+        Config cfg = new Config();
+        cfg.setString("gerrit", null, "basePath", "git");
+        cfg.setString("user", null, "name", "Gerrit Code Review");
+        cfg.setString("user", null, "email", "gerrit@localhost");
+
+        bind(Config.class) //
+            .annotatedWith(GerritServerConfig.class) //
+            .toInstance(cfg);
+
+        bind(PersonIdent.class) //
+            .annotatedWith(GerritPersonIdent.class) //
+            .toProvider(GerritPersonIdentProvider.class);
+
+        bind(GitRepositoryManager.class) //
+            .to(LocalDiskRepositoryManager.class);
       }
     }).getInstance(SchemaUpdater.class);
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
index fe138c6..d95f17a4 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
@@ -17,7 +17,13 @@
 import com.google.gerrit.reviewdb.CurrentSchemaVersion;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.SystemConfig;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.GerritPersonIdentProvider;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SystemConfigProvider;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.schema.Current;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.schema.SchemaVersion;
@@ -25,13 +31,19 @@
 import com.google.gwtorm.client.SchemaFactory;
 import com.google.gwtorm.jdbc.Database;
 import com.google.gwtorm.jdbc.SimpleDataSource;
+import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Key;
 import com.google.inject.Provider;
 
 import junit.framework.TestCase;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+
 import java.io.File;
+import java.io.IOException;
 import java.sql.Connection;
 import java.sql.SQLException;
 import java.util.Properties;
@@ -84,8 +96,33 @@
       database = new Database<ReviewDb>(dataSource, ReviewDb.class);
 
       schemaVersion =
-          Guice.createInjector(new SchemaVersion.Module()).getBinding(
-              Key.get(SchemaVersion.class, Current.class)).getProvider().get();
+          Guice.createInjector(new AbstractModule() {
+            @Override
+            protected void configure() {
+              install(new SchemaVersion.Module());
+
+              bind(File.class) //
+                  .annotatedWith(SitePath.class) //
+                  .toInstance(new File("."));
+
+              Config cfg = new Config();
+              cfg.setString("gerrit", null, "basePath", "git");
+              cfg.setString("user", null, "name", "Gerrit Code Review");
+              cfg.setString("user", null, "email", "gerrit@localhost");
+
+              bind(Config.class) //
+                  .annotatedWith(GerritServerConfig.class) //
+                  .toInstance(cfg);
+
+              bind(PersonIdent.class) //
+                  .annotatedWith(GerritPersonIdent.class) //
+                  .toProvider(GerritPersonIdentProvider.class);
+
+              bind(GitRepositoryManager.class) //
+                  .to(LocalDiskRepositoryManager.class);
+            }
+          }).getBinding(Key.get(SchemaVersion.class, Current.class))
+              .getProvider().get();
     } catch (SQLException e) {
       throw new OrmException(e);
     }
@@ -106,7 +143,14 @@
       created = true;
       final ReviewDb c = open();
       try {
-        new SchemaCreator(new File("."), schemaVersion).create(c);
+        try {
+          new SchemaCreator(new File("."), schemaVersion, null,
+              new PersonIdent("name", "email@site")).create(c);
+        } catch (IOException e) {
+          throw new OrmException("Cannot create in-memory database", e);
+        } catch (ConfigInvalidException e) {
+          throw new OrmException("Cannot create in-memory database", e);
+        }
       } finally {
         c.close();
       }
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
index 32bcf57..0ccf0ec 100644
--- a/gerrit-sshd/pom.xml
+++ b/gerrit-sshd/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-sshd</artifactId>
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index 5b8edf0..61e8bfb 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.sshd.args4j.AccountGroupIdHandler;
+import com.google.gerrit.sshd.args4j.AccountGroupUUIDHandler;
 import com.google.gerrit.sshd.args4j.AccountIdHandler;
 import com.google.gerrit.sshd.args4j.PatchSetIdHandler;
 import com.google.gerrit.sshd.args4j.ProjectControlHandler;
@@ -117,6 +118,7 @@
 
     registerOptionHandler(Account.Id.class, AccountIdHandler.class);
     registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
+    registerOptionHandler(AccountGroup.UUID.class, AccountGroupUUIDHandler.class);
     registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
     registerOptionHandler(ProjectControl.class, ProjectControlHandler.class);
     registerOptionHandler(SocketAddress.class, SocketAddressHandler.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupUUIDHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupUUIDHandler.java
new file mode 100644
index 0000000..90b4987
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupUUIDHandler.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2009 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.sshd.args4j;
+
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+public class AccountGroupUUIDHandler extends OptionHandler<AccountGroup.UUID> {
+  private final GroupCache groupCache;
+
+  @SuppressWarnings({"unchecked", "rawtypes"})
+  @Inject
+  public AccountGroupUUIDHandler(final GroupCache groupCache,
+      @Assisted final CmdLineParser parser, @Assisted final OptionDef option,
+      @Assisted final Setter setter) {
+    super(parser, option, setter);
+    this.groupCache = groupCache;
+  }
+
+  @Override
+  public final int parseArguments(final Parameters params)
+      throws CmdLineException {
+    final String n = params.getParameter(0);
+    final AccountGroup group = groupCache.get(new AccountGroup.NameKey(n));
+    if (group == null) {
+      throw new CmdLineException(owner, "Group \"" + n + "\" does not exist");
+    }
+    setter.addValue(group.getGroupUUID());
+    return 1;
+  }
+
+  @Override
+  public final String getDefaultMetaVariable() {
+    return "GROUP";
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
index 07c573d..feb4891 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -15,22 +15,24 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.config.WildProjectName;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
 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.sshd.AdminCommand;
 import com.google.gerrit.sshd.BaseCommand;
-import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 
 import org.apache.sshd.server.Environment;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
+import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -44,10 +46,10 @@
   private List<ProjectControl> children = new ArrayList<ProjectControl>();
 
   @Inject
-  private ReviewDb db;
+  private ProjectCache projectCache;
 
   @Inject
-  private ProjectCache projectCache;
+  private MetaDataUpdate.User metaDataUpdateFactory;
 
   @Inject
   @WildProjectName
@@ -64,7 +66,7 @@
     });
   }
 
-  private void updateParents() throws OrmException, UnloggedFailure {
+  private void updateParents() throws Failure {
     final StringBuilder err = new StringBuilder();
     final Set<Project.NameKey> grandParents = new HashSet<Project.NameKey>();
     Project.NameKey newParentKey;
@@ -112,22 +114,27 @@
         continue;
       }
 
-      final Project child = db.projects().get(key);
-      if (child == null) {
-        // Race condition? Its in the cache, but not the database.
-        //
-        err.append("error: Project '" + name + "' not found\n");
-        continue;
+      try {
+        MetaDataUpdate md = metaDataUpdateFactory.create(key);
+        try {
+          ProjectConfig config = ProjectConfig.read(md);
+          config.getProject().setParentName(newParentKey.get());
+          md.setMessage("Inherit access from " + newParentKey.get() + "\n");
+          if (!config.commit(md)) {
+            err.append("error: Could not update project " + name + "\n");
+          }
+        } finally {
+          md.close();
+        }
+      } catch (RepositoryNotFoundException notFound) {
+        err.append("error: Project " + name + " not found\n");
+      } catch (IOException e) {
+        throw new Failure(1, "Cannot update project " + name, e);
+      } catch (ConfigInvalidException e) {
+        throw new Failure(1, "Cannot update project " + name, e);
       }
-
-      child.setParent(newParentKey);
-      db.projects().update(Collections.singleton(child));
     }
 
-    // Invalidate all projects in cache since inherited rights were changed.
-    //
-    projectCache.evictAll();
-
     if (err.length() > 0) {
       while (err.charAt(err.length() - 1) == '\n') {
         err.setLength(err.length() - 1);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java
index 0be02fd..77dc8f1 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java
@@ -15,26 +15,30 @@
 package com.google.gerrit.sshd.commands;
 
 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;
+import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.Project.SubmitType;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.ProjectCreatorGroups;
 import com.google.gerrit.server.config.ProjectOwnerGroups;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.git.ReplicationQueue;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.sshd.BaseCommand;
-import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 
 import org.apache.sshd.server.Environment;
+import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -42,7 +46,6 @@
 import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RefUpdate.Result;
-import org.eclipse.jgit.lib.StoredConfig;
 import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -50,7 +53,6 @@
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -63,14 +65,11 @@
   private String projectName;
 
   @Option(name = "--owner", aliases = {"-o"}, usage = "owner(s) of project")
-  private List<AccountGroup.Id> ownerIds;
+  private List<AccountGroup.UUID> ownerIds;
 
   @Option(name = "--parent", aliases = {"-p"}, metaVar = "NAME", usage = "parent project")
   private ProjectControl newParent;
 
-  @Option(name = "--permissions-only", usage = "create project for use only as parent")
-  private boolean permissionsOnly;
-
   @Option(name = "--description", aliases = {"-d"}, metaVar = "DESC", usage = "description of project")
   private String projectDescription = "";
 
@@ -98,18 +97,21 @@
   private boolean createEmptyCommit;
 
   @Inject
-  private ReviewDb db;
-
-  @Inject
   private GitRepositoryManager repoManager;
 
   @Inject
+  private ProjectCache projectCache;
+
+  @Inject
+  private GroupCache groupCache;
+
+  @Inject
   @ProjectCreatorGroups
-  private Set<AccountGroup.Id> projectCreatorGroups;
+  private Set<AccountGroup.UUID> projectCreatorGroups;
 
   @Inject
   @ProjectOwnerGroups
-  private Set<AccountGroup.Id> projectOwnerGroups;
+  private Set<AccountGroup.UUID> projectOwnerGroups;
 
   @Inject
   private IdentifiedUser currentUser;
@@ -121,6 +123,9 @@
   @GerritPersonIdent
   private PersonIdent serverIdent;
 
+  @Inject
+  MetaDataUpdate.User metaDataUpdateFactory;
+
   private Project.NameKey nameKey;
 
   @Override
@@ -136,41 +141,28 @@
           validateParameters();
           nameKey = new Project.NameKey(projectName);
 
-          if (!permissionsOnly) {
-            final Repository repo = repoManager.createRepository(nameKey);
-            try {
-              repo.create(true);
+          final Repository repo = repoManager.createRepository(nameKey);
+          try {
+            RefUpdate u = repo.updateRef(Constants.HEAD);
+            u.disableRefLog();
+            u.link(branch);
 
-              StoredConfig config = repo.getConfig();
-              config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION,
-                  null, ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true);
-              config.save();
-
-              RefUpdate u = repo.updateRef(Constants.HEAD);
-              u.disableRefLog();
-              u.link(branch);
-
-              repoManager.setProjectDescription(nameKey, projectDescription);
-
-              createProject();
-
-              rq.replicateNewProject(nameKey, branch);
-
-              if (createEmptyCommit) {
-                createEmptyCommit(repo, nameKey, branch);
-              }
-            } finally {
-              repo.close();
-            }
-          } else {
             createProject();
+            repoManager.setProjectDescription(nameKey, projectDescription);
+
+            if (createEmptyCommit) {
+              createEmptyCommit(repo, nameKey, branch);
+            }
+
+            rq.replicateNewProject(nameKey, branch);
+          } finally {
+            repo.close();
           }
         } catch (Exception e) {
           p.print("Error when trying to create project: " + e.getMessage()
               + "\n");
           p.flush();
         }
-
       }
     });
   }
@@ -181,9 +173,9 @@
     try {
       CommitBuilder cb = new CommitBuilder();
       cb.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {}));
+      cb.setAuthor(metaDataUpdateFactory.getUserPersonIdent());
       cb.setCommitter(serverIdent);
-      cb.setAuthor(cb.getCommitter());
-      cb.setMessage("Initial empty repository");
+      cb.setMessage("Initial empty repository\n");
 
       ObjectId id = oi.insert(cb);
       oi.flush();
@@ -207,31 +199,41 @@
     }
   }
 
-  private void createProject() throws OrmException {
-    List<RefRight> access = new ArrayList<RefRight>();
-    for (AccountGroup.Id ownerId : ownerIds) {
-      final RefRight.Key prk =
-          new RefRight.Key(nameKey, new RefRight.RefPattern(
-              RefRight.ALL), ApprovalCategory.OWN, ownerId);
-      final RefRight pr = new RefRight(prk);
-      pr.setMaxValue((short) 1);
-      pr.setMinValue((short) 1);
-      access.add(pr);
-    }
-    db.refRights().insert(access);
+  private void createProject() throws IOException, ConfigInvalidException {
+    MetaDataUpdate md = metaDataUpdateFactory.create(nameKey);
+    try {
+      ProjectConfig config = ProjectConfig.read(md);
+      config.load(md);
 
-    final Project newProject = new Project(nameKey);
-    newProject.setDescription(projectDescription);
-    newProject.setSubmitType(submitType);
-    newProject.setUseContributorAgreements(contributorAgreements);
-    newProject.setUseSignedOffBy(signedOffBy);
-    newProject.setUseContentMerge(contentMerge);
-    newProject.setRequireChangeID(requireChangeID);
-    if (newParent != null) {
-      newProject.setParent(newParent.getProject().getNameKey());
-    }
+      Project newProject = config.getProject();
+      newProject.setDescription(projectDescription);
+      newProject.setSubmitType(submitType);
+      newProject.setUseContributorAgreements(contributorAgreements);
+      newProject.setUseSignedOffBy(signedOffBy);
+      newProject.setUseContentMerge(contentMerge);
+      newProject.setRequireChangeID(requireChangeID);
+      if (newParent != null) {
+        newProject.setParentName(newParent.getProject().getName());
+      }
 
-    db.projects().insert(Collections.singleton(newProject));
+      if (!ownerIds.isEmpty()) {
+        AccessSection all = config.getAccessSection(AccessSection.ALL, true);
+        for (AccountGroup.UUID ownerId : ownerIds) {
+          AccountGroup accountGroup = groupCache.get(ownerId);
+          GroupReference group = config.resolve(accountGroup);
+          all.getPermission(Permission.OWNER, true).add(
+              new PermissionRule(group));
+        }
+      }
+
+      md.setMessage("Created project\n");
+      if (!config.commit(md)) {
+        throw new IOException("Cannot create " + projectName);
+      }
+    } finally {
+      md.close();
+    }
+    projectCache.onCreateProject(nameKey);
   }
 
   private void validateParameters() throws Failure {
@@ -246,9 +248,9 @@
 
     if (ownerIds != null && !ownerIds.isEmpty()) {
       ownerIds =
-          new ArrayList<AccountGroup.Id>(new HashSet<AccountGroup.Id>(ownerIds));
+          new ArrayList<AccountGroup.UUID>(new HashSet<AccountGroup.UUID>(ownerIds));
     } else {
-      ownerIds = new ArrayList<AccountGroup.Id>(projectOwnerGroups);
+      ownerIds = new ArrayList<AccountGroup.UUID>(projectOwnerGroups);
     }
 
     while (branch.startsWith("/")) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjects.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjects.java
index bb04f7a..d387949 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjects.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjects.java
@@ -15,15 +15,12 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.config.WildProjectName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 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.sshd.BaseCommand;
-import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 
 import org.apache.sshd.server.Environment;
@@ -44,9 +41,6 @@
   private static final String NOT_VISIBLE_PROJECT = "(x)";
 
   @Inject
-  private ReviewDb db;
-
-  @Inject
   private IdentifiedUser currentUser;
 
   @Inject
@@ -55,12 +49,9 @@
   @Inject
   private GitRepositoryManager repoManager;
 
-  @Inject
-  @WildProjectName
-  private Project.NameKey wildProject;
-
-  @Option(name = "--show-branch", aliases = {"-b"}, usage = "displays the sha of each project in the specified branch")
-  private String showBranch;
+  @Option(name = "--show-branch", aliases = {"-b"}, multiValued = true,
+      usage = "displays the sha of each project in the specified branch")
+  private List<String> showBranch;
 
   @Option(name = "--tree", aliases = {"-t"}, usage = "displays project inheritance in a tree-like format\n" +
       "this option does not work together with the show-branch option")
@@ -93,14 +84,8 @@
     }
 
     try {
-      for (final Project p : db.projects().all()) {
-        if (p.getNameKey().equals(wildProject)) {
-          // This project "doesn't exist". At least not as a repository.
-          //
-          continue;
-        }
-
-        final ProjectState e = projectCache.get(p.getNameKey());
+      for (final Project.NameKey projectName : projectCache.all()) {
+        final ProjectState e = projectCache.get(projectName);
         if (e == null) {
           // If we can't get it from the cache, pretend its not present.
           //
@@ -118,21 +103,43 @@
           }
 
           if (showBranch != null) {
-            final Ref ref = getBranchRef(p.getNameKey());
-            if (ref == null || ref.getObjectId() == null
-                || !pctl.controlForRef(ref.getLeaf().getName()).isVisible()) {
-              // No branch, or the user can't see this branch, so skip it.
-              //
+            final List<Ref> refs = getBranchRefs(projectName);
+            if (refs == null) {
               continue;
             }
 
-            stdout.print(ref.getObjectId().name());
-            stdout.print(' ');
+            boolean hasVisibleRefs = false;
+            for (int i = 0; i < refs.size(); i++) {
+              Ref ref = refs.get(i);
+              if (ref == null
+                || ref.getObjectId() == null
+                || !pctl.controlForRef(ref.getLeaf().getName()).isVisible()) {
+                // No branch, or the user can't see this branch, so remove it.
+                refs.set(i, null);
+              } else {
+                hasVisibleRefs = true;
+              }
+            }
+
+            if (!hasVisibleRefs) {
+             continue;
+            }
+
+            for (Ref ref : refs) {
+              if (ref == null) {
+                // Print stub (forty '-' symbols)
+                stdout.print("----------------------------------------");
+              } else {
+                stdout.print(ref.getObjectId().name());
+              }
+              stdout.print(' ');
+            }
           }
 
-          stdout.print(p.getName() + "\n");
+          stdout.print(projectName.get() + "\n");
         } else {
-          treeMap.put(p.getName(), new TreeNode(p, pctl.isVisible()));
+          treeMap.put(projectName.get(),
+              new TreeNode(pctl.getProject(), pctl.isVisible()));
         }
       }
 
@@ -144,7 +151,7 @@
         for (final TreeNode key : treeMap.values()) {
           final String parentName = key.getParentName();
           if (parentName != null) {
-            final TreeNode node = treeMap.get((String)parentName);
+            final TreeNode node = treeMap.get(parentName);
             if (node != null) {
               node.addChild(key);
             } else {
@@ -161,18 +168,20 @@
         printElement(stdout, fakeRoot, -1, false, sortedNodes.get(sortedNodes.size() - 1));
         stdout.flush();
       }
-    } catch (OrmException e) {
-      throw new Failure(1, "fatal: database error", e);
     } finally {
       stdout.flush();
     }
   }
 
-  private Ref getBranchRef(Project.NameKey projectName) {
+  private List<Ref> getBranchRefs(Project.NameKey projectName) {
     try {
       final Repository r = repoManager.openRepository(projectName);
       try {
-        return r.getRef(showBranch);
+        final List<Ref> result = new ArrayList<Ref>(showBranch.size());
+        for (String branch : showBranch) {
+          result.add(r.getRef(branch));
+        }
+        return result;
       } finally {
         r.close();
       }
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 47bae4f..a8561a5 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
@@ -34,6 +34,7 @@
 import com.google.gerrit.server.patch.PublishComments;
 import com.google.gerrit.server.project.CanSubmitResult;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.workflow.FunctionState;
@@ -202,9 +203,8 @@
     });
   }
 
-  private void approveOne(final PatchSet.Id patchSetId)
-      throws NoSuchChangeException, UnloggedFailure, OrmException,
-             EmailException {
+  private void approveOne(final PatchSet.Id patchSetId) throws
+      NoSuchChangeException, UnloggedFailure, OrmException, EmailException {
 
     final Change.Id changeId = patchSetId.getParentKey();
     ChangeControl changeControl = changeControlFactory.validateFor(changeId);
@@ -224,25 +224,29 @@
 
     publishCommentsFactory.create(patchSetId, changeComment, aps).call();
 
-    if (abandonChange) {
-      if (changeControl.canAbandon()) {
-        ChangeUtil.abandon(patchSetId, currentUser, changeComment, db,
-          abandonedSenderFactory, hooks);
-      } else {
-        throw error("Not permitted to abandon change");
+    try {
+      if (abandonChange) {
+        if (changeControl.canAbandon()) {
+          ChangeUtil.abandon(patchSetId, currentUser, changeComment, db,
+              abandonedSenderFactory, hooks);
+        } else {
+          throw error("Not permitted to abandon change");
+        }
       }
-    }
 
-    if (restoreChange) {
-      if (changeControl.canRestore()) {
-        ChangeUtil.restore(patchSetId, currentUser, changeComment, db,
-          abandonedSenderFactory, hooks);
-      } else {
-        throw error("Not permitted to restore change");
+      if (restoreChange) {
+        if (changeControl.canRestore()) {
+          ChangeUtil.restore(patchSetId, currentUser, changeComment, db,
+              abandonedSenderFactory, hooks);
+        } else {
+          throw error("Not permitted to restore change");
+        }
+        if (submitChange) {
+          changeControl = changeControlFactory.validateFor(changeId);
+        }
       }
-      if (submitChange) {
-        changeControl = changeControlFactory.validateFor(changeId);
-      }
+    } catch (InvalidChangeOperationException e) {
+      throw error(e.getMessage());
     }
 
     if (submitChange) {
@@ -331,7 +335,7 @@
         functionStateFactory.create(changeControl.getChange(), patchSetId,
             Collections.<PatchSetApproval> emptyList());
     psa.setValue(v);
-    fs.normalize(approvalTypes.getApprovalType(psa.getCategoryId()), psa);
+    fs.normalize(approvalTypes.byId(psa.getCategoryId()), psa);
     if (v != psa.getValue()) {
       throw error(ao.name() + "=" + ao.value() + " not permitted");
     }
diff --git a/gerrit-util-cli/pom.xml b/gerrit-util-cli/pom.xml
index 6db5241..cfa302e 100644
--- a/gerrit-util-cli/pom.xml
+++ b/gerrit-util-cli/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-cli</artifactId>
diff --git a/gerrit-util-ssl/pom.xml b/gerrit-util-ssl/pom.xml
index 39d3ce0..312a215 100644
--- a/gerrit-util-ssl/pom.xml
+++ b/gerrit-util-ssl/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-ssl</artifactId>
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index 2d85b74..110f966 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.1-SNAPSHOT</version>
+    <version>2.2-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-war</artifactId>
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/SitePathFromSystemConfigProvider.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/SitePathFromSystemConfigProvider.java
new file mode 100644
index 0000000..3bbcc98
--- /dev/null
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/SitePathFromSystemConfigProvider.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2009 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.httpd;
+
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.SystemConfig;
+import com.google.gerrit.server.config.SitePath;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.File;
+import java.util.List;
+
+/** Provides {@link java.io.File} annotated with {@link SitePath}. */
+class SitePathFromSystemConfigProvider implements Provider<File> {
+  private final File path;
+
+  @Inject
+  SitePathFromSystemConfigProvider(SchemaFactory<ReviewDb> schemaFactory)
+      throws OrmException {
+    path = read(schemaFactory);
+  }
+
+  @Override
+  public File get() {
+    return path;
+  }
+
+  private static File read(SchemaFactory<ReviewDb> schemaFactory)
+      throws OrmException {
+    ReviewDb db = schemaFactory.open();
+    try {
+      List<SystemConfig> all = db.systemConfig().all().toList();
+      switch (all.size()) {
+        case 1:
+          return new File(all.get(0).sitePath);
+        case 0:
+          throw new OrmException("system_config table is empty");
+        default:
+          throw new OrmException("system_config must have exactly 1 row;"
+              + " found " + all.size() + " rows instead");
+      }
+    } finally {
+      db.close();
+    }
+  }
+}
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 19c16ca..f1ac945 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -25,9 +25,9 @@
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.MasterNodeStartup;
 import com.google.gerrit.server.config.SitePath;
-import com.google.gerrit.server.config.SitePathFromSystemConfigProvider;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DatabaseModule;
+import com.google.gerrit.server.schema.SchemaModule;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.MasterCommandModule;
 import com.google.inject.AbstractModule;
@@ -166,6 +166,7 @@
       });
       modules.add(new GerritServerConfigModule());
     }
+    modules.add(new SchemaModule());
     modules.add(new AuthConfigModule());
     return dbInjector.createChildInjector(modules);
   }
diff --git a/pom.xml b/pom.xml
index c20a149..d2f8541 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-parent</artifactId>
   <packaging>pom</packaging>
-  <version>2.1-SNAPSHOT</version>
+  <version>2.2-SNAPSHOT</version>
 
   <name>Gerrit Code Review - Parent</name>
   <url>http://code.google.com/p/gerrit/</url>
@@ -324,6 +324,12 @@
 
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-source-plugin</artifactId>
+          <version>2.1.2</version>
+        </plugin>
+
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-shade-plugin</artifactId>
           <version>1.4</version>
         </plugin>
@@ -376,19 +382,6 @@
           <encoding>UTF-8</encoding>
         </configuration>
       </plugin>
-
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-source-plugin</artifactId>
-        <version>2.1.2</version>
-        <executions>
-          <execution>
-            <goals>
-              <goal>jar</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
     </plugins>
   </build>