Merge branch 'stable'

* stable:
  Update 2.1.7 release notes
  ExportReviewNotes: Default to 2 threads

Change-Id: Icc576083ec6ffa5cbbe5163ab34b8c843dc3ab66
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/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/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/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-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%
rename from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java
rename 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 364d710..4caf8f8 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/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/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/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
index 560b155..d2d4ce0 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/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..3ae3ac5
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -0,0 +1,135 @@
+// 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);
+      }
+    }
+
+    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-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-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/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/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..beeaa59 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
@@ -212,7 +212,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/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 25778a6..4b11849 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,13 +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.server.CurrentUser;
 import com.google.gerrit.server.ReplicationUser;
 import com.google.gerrit.server.config.GitReceivePackGroups;
@@ -29,6 +31,7 @@
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -101,16 +104,18 @@
     ProjectControl create(CurrentUser who, ProjectState ps);
   }
 
-  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(@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.uploadGroups = uploadGroups;
@@ -155,18 +160,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? */
@@ -177,49 +182,60 @@
 
   /** 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);
+    return canPerformOnAnyRef(Permission.PUSH)
+        || canPerformOnAnyRef(Permission.PUSH_TAG);
   }
 
-  // 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 = user.getEffectiveGroups();
+  private boolean canPerformOnAnyRef(String permissionName) {
+    final Set<AccountGroup.UUID> groups = user.getEffectiveGroups();
 
-    for (final RefRight pr : state.getAllRights(actionId, true)) {
-      if (groups.contains(pr.getAccountGroupId())
-          && pr.getMaxValue() >= requireValue) {
-        return true;
+    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(ApprovalCategory.Id actionId,
-      short requireValue) {
+  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;
@@ -229,14 +245,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, user.getEffectiveGroups());
   }
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..c5c974d 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();
+        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..c5ca308 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;
@@ -50,27 +62,40 @@
   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..b83f540 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_53.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..8a04071
--- /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/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 cf5d264..ab5d75f 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,63 +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(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;
+    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/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..b70b743 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,10 +49,6 @@
   @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;
 
@@ -93,14 +83,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,7 +102,7 @@
           }
 
           if (showBranch != null) {
-            final Ref ref = getBranchRef(p.getNameKey());
+            final Ref ref = getBranchRef(projectName);
             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.
@@ -130,9 +114,10 @@
             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 +129,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,8 +146,6 @@
         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();
     }
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..0a6033b 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
@@ -331,7 +331,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-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);
   }