Merge "Fix query by 'label:Verified=0'"
diff --git a/Documentation/config-cla.txt b/Documentation/config-cla.txt
index e63486f..6404d4e 100644
--- a/Documentation/config-cla.txt
+++ b/Documentation/config-cla.txt
@@ -1,5 +1,5 @@
 Gerrit Code Review - Contributor Agreements
-============================================
+===========================================
 
 Users can be required to sign one or more contributor agreements before
 being able to submit a change in a project.
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 2fafdab..006ad26 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -70,13 +70,13 @@
 
 For security issues it is important that they are only announced
 *after* fixed versions for all relevant releases have been published.
-Because of this `security-fix` releases can't be prepared in the public
+Because of this, `security-fix` releases can't be prepared in the public
 `gerrit` project.
 
 `security-fix` releases are prepared in the `gerrit-security-fixes`
 project which is only readable by the Gerrit Maintainers. Only after
-a `security-fix` release has been published the commits/tags done in
-the `gerrit-security-fixes` project will be taken over into the public
+a `security-fix` release has been published will the commits/tags made in
+the `gerrit-security-fixes` project be taken over into the public
 `gerrit` project.
 
 
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index 9aaac0e..7e77044 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -243,6 +243,78 @@
   }
 ----
 
+[[groups]]
+/groups/ (List Groups)
+~~~~~~~~~~~~~~~~~~~~~~
+Lists the groups accessible by the caller. This is the same as
+using the link:cmd-ls-groups.html[ls-groups] command over SSH,
+and accepts the same options as query parameters.
+
+----
+  GET /groups/ HTTP/1.0
+
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "Administrators": {
+      "kind": "gerritcodereview#group",
+      "id": "uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389",
+      "uuid": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
+      "group_id": 1,
+      "description": "Gerrit Site Administrators",
+      "is_visible_to_all": false,
+      "owner_uuid": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+    },
+    "Anonymous Users": {
+      "kind": "gerritcodereview#group",
+      "id": "uuid-global%3AAnonymous-Users",
+      "uuid": "global:Anonymous-Users",
+      "group_id": 2,
+      "description": "Any user, signed-in or not",
+      "is_visible_to_all": false,
+      "owner_uuid": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+    },
+    "MyProject_Committers": {
+      "kind": "gerritcodereview#group",
+      "id": "uuid-834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7",
+      "uuid": "834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7",
+      "group_id": 6,
+      "is_visible_to_all": true,
+      "owner_uuid": "834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7"
+    },
+    "Non-Interactive Users": {
+      "kind": "gerritcodereview#group",
+      "id": "uuid-5057f3cbd3519d6ab69364429a89ffdffba50f73",
+      "uuid": "5057f3cbd3519d6ab69364429a89ffdffba50f73",
+      "group_id": 4,
+      "description": "Users who perform batch actions on Gerrit",
+      "is_visible_to_all": false,
+      "owner_uuid": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+    },
+    "Project Owners": {
+      "kind": "gerritcodereview#group",
+      "id": "uuid-global%3AProject-Owners",
+      "uuid": "global:Project-Owners",
+      "group_id": 5,
+      "description": "Any owner of the project",
+      "is_visible_to_all": false,
+      "owner_uuid": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+    },
+    "Registered Users": {
+      "kind": "gerritcodereview#group",
+      "id": "uuid-global%3ARegistered-Users",
+      "uuid": "global:Registered-Users",
+      "group_id": 3,
+      "description": "Any signed-in user",
+      "is_visible_to_all": false,
+      "owner_uuid": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+    }
+  }
+----
+
 [[changes]]
 /changes/ (Query Changes)
 ~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
index fc5e49b..0e832dd 100644
--- a/Documentation/user-notify.txt
+++ b/Documentation/user-notify.txt
@@ -18,9 +18,9 @@
 `All-Projects` can be watched to watch all projects that
 are visible to the user.
 
-Change search expressions can be used to filter change notifications
-to specific subsets, for example `branch:master` to only see changes
-proposed for the master branch.
+link:user-search.html[Change search expressions] can be used to filter
+change notifications to specific subsets, for example `branch:master`
+to only see changes proposed for the master branch.
 
 Project Level Settings
 ----------------------
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
index aa212f9..c2d03ab 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.client.ContactInformation;
 import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -61,9 +60,6 @@
   @SignInRequired
   void myExternalIds(AsyncCallback<List<AccountExternalId>> callback);
 
-  @SignInRequired
-  void myGroups(AsyncCallback<List<AccountGroup>> callback);
-
   @Audit
   @SignInRequired
   void deleteExternalIds(Set<AccountExternalId.Key> keys,
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 5cb7fa2..96a80cc 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
@@ -17,7 +17,7 @@
 import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -31,10 +31,6 @@
 public interface GroupAdminService extends RemoteJsonService {
   @Audit
   @SignInRequired
-  void visibleGroups(AsyncCallback<GroupList> callback);
-
-  @Audit
-  @SignInRequired
   void createGroup(String newName, AsyncCallback<AccountGroup.Id> callback);
 
   @Audit
@@ -74,8 +70,8 @@
 
   @Audit
   @SignInRequired
-  void addGroupInclude(AccountGroup.Id groupId, String groupName,
-      AsyncCallback<GroupDetail> callback);
+  void addGroupInclude(AccountGroup.Id groupId, AccountGroup.UUID incGroupUUID,
+      String incGroupName, AsyncCallback<GroupDetail> callback);
 
   @Audit
   @SignInRequired
@@ -85,5 +81,5 @@
   @Audit
   @SignInRequired
   void deleteGroupIncludes(AccountGroup.Id groupId,
-      Set<AccountGroupInclude.Key> keys, AsyncCallback<VoidResult> callback);
+      Set<AccountGroupIncludeByUuid.Key> keys, AsyncCallback<VoidResult> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
index 01c7985..24e3b08 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 
 import java.util.List;
@@ -25,7 +25,7 @@
   public GroupInfoCache groups;
   public AccountGroup group;
   public List<AccountGroupMember> members;
-  public List<AccountGroupInclude> includes;
+  public List<AccountGroupIncludeByUuid> includes;
   public GroupReference ownerGroup;
   public boolean canModify;
 
@@ -48,7 +48,7 @@
     members = m;
   }
 
-  public void setIncludes(List<AccountGroupInclude> i) {
+  public void setIncludes(List<AccountGroupIncludeByUuid> i) {
     includes = i;
   }
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java
index 547a5f4..f01365b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java
@@ -18,7 +18,7 @@
 
 /** Summary information about an {@link AccountGroup}, for simple tabular displays. */
 public class GroupInfo {
-  protected AccountGroup.Id id;
+  protected AccountGroup.UUID uuid;
   protected String name;
   protected String description;
 
@@ -32,8 +32,8 @@
    * lookup has failed and a stale group id has been discovered in the data
    * store.
    */
-  public GroupInfo(final AccountGroup.Id id) {
-    this.id = id;
+  public GroupInfo(final AccountGroup.UUID uuid) {
+    this.uuid = uuid;
   }
 
   /**
@@ -42,14 +42,14 @@
    * @param a the data store record holding the specific group details.
    */
   public GroupInfo(final AccountGroup a) {
-    id = a.getId();
+    uuid = a.getGroupUUID();
     name = a.getName();
     description = a.getDescription();
   }
 
   /** @return the unique local id of the group */
-  public AccountGroup.Id getId() {
-    return id;
+  public AccountGroup.UUID getId() {
+    return uuid;
   }
 
   /** @return the name of the group; null if not supplied */
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfoCache.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfoCache.java
index 6a5dd5c..085973c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfoCache.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfoCache.java
@@ -33,13 +33,13 @@
     return EMPTY;
   }
 
-  protected Map<AccountGroup.Id, GroupInfo> groups;
+  protected Map<AccountGroup.UUID, GroupInfo> groups;
 
   protected GroupInfoCache() {
   }
 
   public GroupInfoCache(final Iterable<GroupInfo> list) {
-    groups = new HashMap<AccountGroup.Id, GroupInfo>();
+    groups = new HashMap<AccountGroup.UUID, GroupInfo>();
     for (final GroupInfo gi : list) {
       groups.put(gi.getId(), gi);
     }
@@ -58,15 +58,15 @@
    * @param id the id desired.
    * @return info block for the group.
    */
-  public GroupInfo get(final AccountGroup.Id id) {
-    if (id == null) {
+  public GroupInfo get(final AccountGroup.UUID uuid) {
+    if (uuid == null) {
       return null;
     }
 
-    GroupInfo r = groups.get(id);
+    GroupInfo r = groups.get(uuid);
     if (r == null) {
-      r = new GroupInfo(id);
-      groups.put(id, r);
+      r = new GroupInfo(uuid);
+      groups.put(uuid, r);
     }
     return r;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupList.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupList.java
deleted file mode 100644
index b3095cd..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupList.java
+++ /dev/null
@@ -1,48 +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.common.data;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-
-import java.util.List;
-
-public class GroupList {
-  protected List<AccountGroup> groups;
-  protected boolean canCreateGroup;
-
-  protected GroupList() {
-  }
-
-  public GroupList(final List<AccountGroup> groups, final boolean canCreateGroup) {
-    this.groups = groups;
-    this.canCreateGroup = canCreateGroup;
-  }
-
-  public List<AccountGroup> getGroups() {
-    return groups;
-  }
-
-  public void setGroups(List<AccountGroup> groups) {
-    this.groups = groups;
-  }
-
-  public boolean isCanCreateGroup() {
-    return canCreateGroup;
-  }
-
-  public void setCanCreateGroup(boolean set) {
-    canCreateGroup = set;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
index 6cb749d..8b3bf56 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
@@ -15,10 +15,8 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.admin.GroupTable;
+import com.google.gerrit.client.groups.GroupMap;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-
-import java.util.List;
 
 public class MyGroupsScreen extends SettingsScreen {
   private GroupTable groups;
@@ -33,11 +31,11 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.ACCOUNT_SEC.myGroups(new ScreenLoadCallback<List<AccountGroup>>(this) {
+    GroupMap.my(new ScreenLoadCallback<GroupMap>(this) {
       @Override
-      public void preDisplay(final List<AccountGroup> result) {
+      protected void preDisplay(final GroupMap result) {
         groups.display(result);
-      }
-    });
+        groups.finishDisplay();
+      }});
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index 4c0b1ba..d6ab9b6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -25,11 +25,10 @@
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.common.data.GroupDetail;
-import com.google.gerrit.common.data.GroupInfo;
 import com.google.gerrit.common.data.GroupInfoCache;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -110,14 +109,15 @@
   }
 
   private void initIncludeList() {
+    final AccountGroupSuggestOracle oracle = new AccountGroupSuggestOracle();
     addIncludeBox =
       new AddMemberBox(Util.C.buttonAddIncludedGroup(),
-          Util.C.defaultAccountGroupName(), new AccountGroupSuggestOracle());
+          Util.C.defaultAccountGroupName(), oracle);
 
     addIncludeBox.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
-        doAddNewInclude();
+        doAddNewInclude(oracle);
       }
     });
 
@@ -194,14 +194,14 @@
         });
   }
 
-  void doAddNewInclude() {
+  void doAddNewInclude(final AccountGroupSuggestOracle oracle) {
     final String groupName = addIncludeBox.getText();
     if (groupName.length() == 0) {
       return;
     }
 
     addIncludeBox.setEnabled(false);
-    Util.GROUP_SVC.addGroupInclude(getGroupId(), groupName,
+    Util.GROUP_SVC.addGroupInclude(getGroupId(), oracle.getUUID(groupName), groupName,
         new GerritCallback<GroupDetail>() {
           public void onSuccess(final GroupDetail result) {
             addIncludeBox.setEnabled(true);
@@ -298,7 +298,7 @@
     }
   }
 
-  private class IncludeTable extends FancyFlexTable<AccountGroupInclude> {
+  private class IncludeTable extends FancyFlexTable<AccountGroupIncludeByUuid> {
     private boolean enabled = true;
 
     IncludeTable() {
@@ -314,7 +314,7 @@
     void setEnabled(final boolean enabled) {
       this.enabled = enabled;
       for (int row = 1; row < table.getRowCount(); row++) {
-        final AccountGroupInclude k = getRowItem(row);
+        final AccountGroupIncludeByUuid k = getRowItem(row);
         if (k != null) {
           ((CheckBox) table.getWidget(row, 1)).setEnabled(enabled);
         }
@@ -322,10 +322,10 @@
     }
 
     void deleteChecked() {
-      final HashSet<AccountGroupInclude.Key> keys =
-          new HashSet<AccountGroupInclude.Key>();
+      final HashSet<AccountGroupIncludeByUuid.Key> keys =
+          new HashSet<AccountGroupIncludeByUuid.Key>();
       for (int row = 1; row < table.getRowCount(); row++) {
-        final AccountGroupInclude k = getRowItem(row);
+        final AccountGroupIncludeByUuid k = getRowItem(row);
         if (k != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
           keys.add(k.getKey());
         }
@@ -335,7 +335,7 @@
             new GerritCallback<VoidResult>() {
               public void onSuccess(final VoidResult result) {
                 for (int row = 1; row < table.getRowCount();) {
-                  final AccountGroupInclude k = getRowItem(row);
+                  final AccountGroupIncludeByUuid k = getRowItem(row);
                   if (k != null && keys.contains(k.getKey())) {
                     table.removeRow(row);
                   } else {
@@ -347,11 +347,11 @@
       }
     }
 
-    void display(final List<AccountGroupInclude> result) {
+    void display(final List<AccountGroupIncludeByUuid> result) {
       while (1 < table.getRowCount())
         table.removeRow(table.getRowCount() - 1);
 
-      for (final AccountGroupInclude k : result) {
+      for (final AccountGroupIncludeByUuid k : result) {
         final int row = table.getRowCount();
         table.insertRow(row);
         applyDataRowStyle(row);
@@ -359,15 +359,18 @@
       }
     }
 
-    void populate(final int row, final AccountGroupInclude k) {
-      AccountGroup.Id id = k.getIncludeId();
-      GroupInfo group = groups.get(id);
+    void populate(final int row, final AccountGroupIncludeByUuid k) {
+      AccountGroup.UUID uuid = k.getIncludeUUID();
       CheckBox checkBox = new CheckBox();
       table.setWidget(row, 1, checkBox);
       checkBox.setEnabled(enabled);
-      table.setWidget(row, 2,
-          new Hyperlink(group.getName(), Dispatcher.toGroup(id)));
-      table.setText(row, 3, groups.get(id).getDescription());
+      if (AccountGroup.isInternalGroup(uuid)) {
+        table.setWidget(row, 2,
+            new Hyperlink(groups.get(uuid).getName(), Dispatcher.toGroup(uuid)));
+        table.setText(row, 3, groups.get(uuid).getDescription());
+      } else {
+        table.setText(row, 2, uuid.get());
+      }
 
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
       fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
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 d278e1f..d0a3329 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
@@ -97,6 +97,7 @@
   String groupItemHelp();
 
   String groupListTitle();
+  String groupFilter();
   String createGroupTitle();
   String groupTabGeneral();
   String groupTabMembers();
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 765ed09..7d6859c 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
@@ -77,6 +77,7 @@
 groupItemHelp = group
 
 groupListTitle = Groups
+groupFilter = Filter
 createGroupTitle = Create Group
 groupTabGeneral = General
 groupTabMembers = Members
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
index dae0a03..555d023 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
@@ -235,8 +235,22 @@
         new GerritCallback<VoidResult>() {
           @Override
           public void onSuccess(VoidResult result) {
+            String nameWithoutSuffix = projectName;
+            if (nameWithoutSuffix.endsWith(".git")) {
+              // Be nice and drop the trailing ".git" suffix, which we never
+              // keep in our database, but clients might mistakenly provide
+              // anyway.
+              //
+              nameWithoutSuffix = nameWithoutSuffix.substring(0, //
+                  nameWithoutSuffix.length() - 4);
+              while (nameWithoutSuffix.endsWith("/")) {
+                nameWithoutSuffix = nameWithoutSuffix.substring(//
+                    0, nameWithoutSuffix.length() - 1);
+              }
+            }
+
             History.newItem(Dispatcher.toProjectAdmin(new Project.NameKey(
-                projectName), ProjectScreen.INFO));
+                nameWithoutSuffix), ProjectScreen.INFO));
           }
 
           @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index 3157d97..8c54b58 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
@@ -14,36 +14,82 @@
 
 package com.google.gerrit.client.admin;
 
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.groups.GroupMap;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.AccountScreen;
+import com.google.gerrit.client.ui.FilteredUserInterface;
+import com.google.gerrit.client.ui.IgnoreOutdatedFilterResultsCallbackWrapper;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.GroupList;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
 
-public class GroupListScreen extends AccountScreen {
+public class GroupListScreen extends AccountScreen implements FilteredUserInterface {
   private GroupTable groups;
+  private NpTextBox filterTxt;
+  private String subname;
 
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.GROUP_SVC
-        .visibleGroups(new ScreenLoadCallback<GroupList>(this) {
-          @Override
-          protected void preDisplay(GroupList result) {
-            groups.display(result.getGroups());
-            groups.finishDisplay();
-          }
-        });
+    refresh();
+  }
+
+  private void refresh() {
+    GroupMap.match(subname,
+        new IgnoreOutdatedFilterResultsCallbackWrapper<GroupMap>(this,
+            new ScreenLoadCallback<GroupMap>(this) {
+              @Override
+              protected void preDisplay(final GroupMap result) {
+                groups.display(result, subname);
+                groups.finishDisplay();
+              }
+            }));
+  }
+
+  @Override
+  public String getCurrentFilter() {
+    return subname;
   }
 
   @Override
   protected void onInitUI() {
     super.onInitUI();
     setPageTitle(Util.C.groupListTitle());
+    initPageHeader();
 
     groups = new GroupTable(true /* hyperlink to admin */, PageLinks.ADMIN_GROUPS);
     add(groups);
   }
 
+  private void initPageHeader() {
+    final HorizontalPanel hp = new HorizontalPanel();
+    hp.setStyleName(Gerrit.RESOURCES.css().projectFilterPanel());
+    final Label filterLabel = new Label(Util.C.projectFilter());
+    filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
+    hp.add(filterLabel);
+    filterTxt = new NpTextBox();
+    filterTxt.setValue(subname);
+    filterTxt.addKeyUpHandler(new KeyUpHandler() {
+      @Override
+      public void onKeyUp(KeyUpEvent event) {
+        subname = filterTxt.getValue();
+        refresh();
+      }
+    });
+    hp.add(filterTxt);
+    add(hp);
+  }
+
+  @Override
+  public void onShowView() {
+    super.onShowView();
+    filterTxt.setFocus(true);
+  }
+
   @Override
   public void registerKeys() {
     super.registerKeys();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
index 3116916..6a16f1a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
@@ -16,9 +16,10 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.client.groups.GroupInfo;
+import com.google.gerrit.client.groups.GroupMap;
+import com.google.gerrit.client.ui.HighlightingInlineHyperlink;
 import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.user.client.History;
@@ -26,10 +27,12 @@
 import com.google.gwt.user.client.ui.HTMLTable.Cell;
 import com.google.gwt.user.client.ui.Image;
 
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 
 
-public class GroupTable extends NavigationTable<AccountGroup> {
+public class GroupTable extends NavigationTable<GroupInfo> {
   private static final int NUM_COLS = 3;
 
   private final boolean enableLink;
@@ -64,35 +67,46 @@
   }
 
   @Override
-  protected Object getRowItemKey(final AccountGroup item) {
-    return item.getId();
+  protected Object getRowItemKey(final GroupInfo item) {
+    return item.getGroupId();
   }
 
   @Override
   protected void onOpenRow(final int row) {
-    History.newItem(Dispatcher.toGroup(getRowItem(row).getId()));
+    History.newItem(Dispatcher.toGroup(getRowItem(row).getGroupId()));
   }
 
-  public void display(final List<AccountGroup> result) {
+  public void display(final GroupMap groups) {
+    display(groups, null);
+  }
+
+  public void display(final GroupMap groups, final String toHighlight) {
     while (1 < table.getRowCount())
       table.removeRow(table.getRowCount() - 1);
 
-    for(AccountGroup group : result) {
+    List<GroupInfo> list = groups.values().asList();
+    Collections.sort(list, new Comparator<GroupInfo>() {
+      @Override
+      public int compare(GroupInfo a, GroupInfo b) {
+        return a.name().compareTo(b.name());
+      }
+    });
+    for(GroupInfo group : list) {
       final int row = table.getRowCount();
       table.insertRow(row);
       applyDataRowStyle(row);
-      populate(row, group);
+      populate(row, group, toHighlight);
     }
   }
 
-  void populate(final int row, final AccountGroup k) {
+  void populate(final int row, final GroupInfo k, final String toHighlight) {
     if (enableLink) {
-      table.setWidget(row, 1, new Hyperlink(k.getName(),
-          Dispatcher.toGroup(k.getId())));
+      table.setWidget(row, 1, new HighlightingInlineHyperlink(k.name(),
+          Dispatcher.toGroup(k.getGroupId()), toHighlight));
     } else {
-      table.setText(row, 1, k.getName());
+      table.setText(row, 1, k.name());
     }
-    table.setText(row, 2, k.getDescription());
+    table.setText(row, 2, k.description());
     if (k.isVisibleToAll()) {
       table.setWidget(row, 3, new Image(Gerrit.RESOURCES.greenCheck()));
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
index f702820..4795bda 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -20,7 +20,9 @@
 import com.google.gerrit.client.projects.ProjectInfo;
 import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.FilteredUserInterface;
 import com.google.gerrit.client.ui.HighlightingInlineHyperlink;
+import com.google.gerrit.client.ui.IgnoreOutdatedFilterResultsCallbackWrapper;
 import com.google.gerrit.client.ui.ProjectSearchLink;
 import com.google.gerrit.client.ui.ProjectsTable;
 import com.google.gerrit.client.ui.Screen;
@@ -34,7 +36,7 @@
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 
-public class ProjectListScreen extends Screen {
+public class ProjectListScreen extends Screen implements FilteredUserInterface {
   private ProjectsTable projects;
   private NpTextBox filterTxt;
   private String subname;
@@ -46,25 +48,19 @@
   }
 
   private void refresh() {
-    final String mySubname = subname;
-    ProjectMap.match(subname, new ScreenLoadCallback<ProjectMap>(this) {
-      @Override
-      protected void preDisplay(final ProjectMap result) {
-        if ((mySubname == null && subname == null)
-            || (mySubname != null && mySubname.equals(subname))) {
-          display(result);
-        }
-        // Else ignore the result, the user has already changed subname and
-        // the result is not relevant anymore. If multiple RPC's are fired
-        // the results may come back out-of-order and a non-relevant result
-        // could overwrite the correct result if not ignored.
-      }
-    });
+    ProjectMap.match(subname,
+        new IgnoreOutdatedFilterResultsCallbackWrapper<ProjectMap>(this,
+            new ScreenLoadCallback<ProjectMap>(this) {
+              @Override
+              protected void preDisplay(final ProjectMap result) {
+                projects.display(result);
+              }
+            }));
   }
 
-  private void display(final ProjectMap result) {
-    projects.display(result);
-    projects.finishDisplay();
+  @Override
+  public String getCurrentFilter() {
+    return subname;
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
index 3dcd5d9..509f34c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
@@ -98,14 +98,6 @@
   public ChangeScreen(final Change.Id toShow) {
     changeId = toShow;
     openPatchSetId = null;
-
-    // If we have any diff stored, make sure they are applicable to the
-    // current change, discard them otherwise.
-    //
-    if (currentChangeId != null && !currentChangeId.equals(toShow)) {
-      diffBaseId = null;
-    }
-    currentChangeId = toShow;
   }
 
   public ChangeScreen(final PatchSet.Id toShow) {
@@ -271,6 +263,7 @@
 
   private void display(final ChangeDetail detail) {
     displayTitle(detail.getChange().getKey(), detail.getChange().getSubject());
+    discardDiffBaseIfNotApplicable(detail.getChange().getId());
 
     if (Status.MERGED == detail.getChange().getStatus()) {
       includedInPanel.setVisible(true);
@@ -291,6 +284,7 @@
     neededBy.display(detail.getNeededBy());
     approvals.display(detail);
 
+    patchesList.clear();
     if (detail.getCurrentPatchSetDetail().getInfo().getParents().size() > 1) {
       patchesList.addItem(Util.C.autoMerge());
     } else {
@@ -354,6 +348,13 @@
     patchSetsBlock.setRegisterKeys(true);
   }
 
+  private static void discardDiffBaseIfNotApplicable(final Change.Id toShow) {
+    if (currentChangeId != null && !currentChangeId.equals(toShow)) {
+      diffBaseId = null;
+    }
+    currentChangeId = toShow;
+  }
+
   private void addComments(final ChangeDetail detail) {
     comments.clear();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index ef1596c..5d52a07 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
@@ -1158,6 +1158,8 @@
   margin-left: 1em;
   margin-right: 5em;
   font-weight: bold;
+  font-size: medium;
+  font-family: Arial Unicode;
 }
 
 /** Patch History Table **/
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
new file mode 100644
index 0000000..5fed85d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2013 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.groups;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class GroupInfo extends JavaScriptObject {
+  public final AccountGroup.Id getGroupId() {
+    return new AccountGroup.Id(groupId());
+  }
+
+  public final native int groupId() /*-{ return this.group_id; }-*/;
+  public final native String name() /*-{ return this.name; }-*/;
+  public final native String uuid() /*-{ return this.uuid; }-*/;
+  public final native String description() /*-{ return this.description; }-*/;
+  public final native boolean isVisibleToAll() /*-{ return this['is_visible_to_all'] ? true : false; }-*/;
+  public final native String ownerUuid() /*-{ return this.owner_uuid; }-*/;
+
+  protected GroupInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
new file mode 100644
index 0000000..e3a984f0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2013 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.groups;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/** Groups available from {@code /groups/}. */
+public class GroupMap extends NativeMap<GroupInfo> {
+  public static void all(AsyncCallback<GroupMap> callback) {
+    new RestApi("/groups/")
+        .get(NativeMap.copyKeysIntoChildren(callback));
+  }
+
+  public static void my(AsyncCallback<GroupMap> callback) {
+    new RestApi("/groups/")
+        .addParameter("user", Gerrit.getUserAccount().getId().get())
+        .get(NativeMap.copyKeysIntoChildren(callback));
+  }
+
+  public static void match(String match, AsyncCallback<GroupMap> cb) {
+    if (match == null || "".equals(match)) {
+      all(cb);
+    } else {
+      new RestApi("/groups/")
+          .addParameter("m", match)
+          .get(NativeMap.copyKeysIntoChildren(cb));
+    }
+  }
+
+  protected GroupMap() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
index bd02a72..8e4583a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
@@ -58,6 +58,7 @@
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -237,6 +238,27 @@
     render(s, d);
   }
 
+  protected boolean hasDifferences(final PatchScript script) {
+    // True if there are differences between the two patch sets
+    boolean hasEdits = !script.getEdits().isEmpty();
+    // True if this change is a mode change or a pure rename/copy
+    boolean hasMeta = !script.getPatchHeader().isEmpty();
+
+    return hasEdits || hasMeta;
+  }
+
+  protected void appendNoDifferences(SafeHtmlBuilder m) {
+    m.openTr();
+    m.openTd();
+    m.setAttribute("colspan", 5);
+    m.openDiv();
+    m.addStyleName(Gerrit.RESOURCES.css().patchNoDifference());
+    m.append(PatchUtil.C.noDifference());
+    m.closeDiv();
+    m.closeTd();
+    m.closeTr();
+  }
+
   protected SparseHtmlFile getSparseHtmlFileA(PatchScript s) {
     AccountDiffPreference dp = new AccountDiffPreference(s.getDiffPrefs());
     dp.setShowWhitespaceErrors(false);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
index 36eae80..1dd7c6a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
@@ -39,7 +39,6 @@
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Label;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
@@ -103,7 +102,6 @@
   private HistoryTable historyTable;
   private FlowPanel topPanel;
   private FlowPanel contentPanel;
-  private Label noDifference;
   private AbstractPatchContentTable contentTable;
   private CommitMessageBlock commitMessageBlock;
   private NavLinks topNav;
@@ -250,10 +248,6 @@
     topPanel = new FlowPanel();
     add(topPanel);
 
-    noDifference = new Label(PatchUtil.C.noDifference());
-    noDifference.setStyleName(Gerrit.RESOURCES.css().patchNoDifference());
-    noDifference.setVisible(false);
-
     contentTable = createContentTable();
     contentTable.fileList = fileList;
 
@@ -269,7 +263,6 @@
       contentPanel.setStyleName(Gerrit.RESOURCES.css().unifiedTable());
     }
 
-    contentPanel.add(noDifference);
     contentPanel.add(contentTable);
     add(contentPanel);
     add(bottomNav);
@@ -433,7 +426,6 @@
     // True if this change is a mode change or a pure rename/copy
     boolean hasMeta = !script.getPatchHeader().isEmpty();
 
-    boolean hasDifferences = hasEdits || hasMeta;
     boolean pureMetaChange = !hasEdits && hasMeta;
 
     if (contentTable instanceof SideBySideTable && pureMetaChange && !contentTable.isDisplayBinary) {
@@ -449,12 +441,11 @@
       setToken(Dispatcher.toPatchUnified(idSideA, patchKey));
     }
 
-    if (hasDifferences) {
-      contentTable.display(patchKey, idSideA, idSideB, script, patchSetDetail);
-      contentTable.display(script.getCommentDetail(), script.isExpandAllComments());
-      contentTable.finishDisplay();
-    }
-    showPatch(hasDifferences);
+    contentTable.display(patchKey, idSideA, idSideB, script, patchSetDetail);
+    contentTable.display(script.getCommentDetail(), script.isExpandAllComments());
+    contentTable.finishDisplay();
+    contentTable.setRegisterKeys(isCurrentView());
+
     settingsPanel.setEnableSmallFileFeatures(!script.isHugeFile());
     settingsPanel.setEnableIntralineDifference(script.hasIntralineDifference());
     settingsPanel.setEnabled(true);
@@ -496,12 +487,6 @@
     }
   }
 
-  private void showPatch(final boolean showPatch) {
-    noDifference.setVisible(!showPatch);
-    contentTable.setVisible(showPatch);
-    contentTable.setRegisterKeys(isCurrentView() && showPatch);
-  }
-
   public void setTopView(TopView tv) {
     topView = tv;
     topPanel.clear();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
index 4ce402e..d57830a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
@@ -105,84 +105,86 @@
         lines.add(null);
       }
 
-      int lastA = 0;
-      int lastB = 0;
-      final boolean ignoreWS = script.isIgnoreWhitespace();
-      a = getSparseHtmlFileA(script);
-      b = getSparseHtmlFileB(script);
-      final boolean intraline =
-          script.getDiffPrefs().isIntralineDifference()
-              && script.hasIntralineDifference();
-      for (final EditList.Hunk hunk : script.getHunks()) {
-        if (!hunk.isStartOfFile()) {
-          appendSkipLine(nc, hunk.getCurB() - lastB);
-          lines.add(new SkippedLine(lastA, lastB, hunk.getCurB() - lastB));
-        }
+      if (hasDifferences(script)) {
+        int lastA = 0;
+        int lastB = 0;
+        final boolean ignoreWS = script.isIgnoreWhitespace();
+        a = getSparseHtmlFileA(script);
+        b = getSparseHtmlFileB(script);
+        final boolean intraline =
+            script.getDiffPrefs().isIntralineDifference()
+                && script.hasIntralineDifference();
+        for (final EditList.Hunk hunk : script.getHunks()) {
+          if (!hunk.isStartOfFile()) {
+            appendSkipLine(nc, hunk.getCurB() - lastB);
+            lines.add(new SkippedLine(lastA, lastB, hunk.getCurB() - lastB));
+          }
 
-        while (hunk.next()) {
-          if (hunk.isContextLine()) {
-            openLine(nc);
-            final SafeHtml ctx = a.getSafeHtmlLine(hunk.getCurA());
-            appendLineNumber(nc, hunk.getCurA(), false);
-            appendLineText(nc, CONTEXT, ctx, false, false);
-            if (ignoreWS && b.contains(hunk.getCurB())) {
-              appendLineText(nc, CONTEXT, b, hunk.getCurB(), false);
-            } else {
-              appendLineText(nc, CONTEXT, ctx, false, false);
-            }
-            appendLineNumber(nc, hunk.getCurB(), true);
-            closeLine(nc);
-            hunk.incBoth();
-            lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
-
-          } else if (hunk.isModifiedLine()) {
-            final boolean del = hunk.isDeletedA();
-            final boolean ins = hunk.isInsertedB();
-            final boolean full =
-                intraline && hunk.getCurEdit().getType() != Edit.Type.REPLACE;
-            openLine(nc);
-
-            if (del) {
+          while (hunk.next()) {
+            if (hunk.isContextLine()) {
+              openLine(nc);
+              final SafeHtml ctx = a.getSafeHtmlLine(hunk.getCurA());
               appendLineNumber(nc, hunk.getCurA(), false);
-              appendLineText(nc, DELETE, a, hunk.getCurA(), full);
-              hunk.incA();
-            } else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
-              appendLineNumber(nc, false);
-              appendLineNone(nc, DELETE);
-            } else {
-              appendLineNumber(nc, false);
-              appendLineNone(nc, CONTEXT);
-            }
-
-            if (ins) {
-              appendLineText(nc, INSERT, b, hunk.getCurB(), full);
+              appendLineText(nc, CONTEXT, ctx, false, false);
+              if (ignoreWS && b.contains(hunk.getCurB())) {
+                appendLineText(nc, CONTEXT, b, hunk.getCurB(), false);
+              } else {
+                appendLineText(nc, CONTEXT, ctx, false, false);
+              }
               appendLineNumber(nc, hunk.getCurB(), true);
-              hunk.incB();
-            } else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
-              appendLineNone(nc, INSERT);
-              appendLineNumber(nc, true);
-            } else {
-              appendLineNone(nc, CONTEXT);
-              appendLineNumber(nc, true);
-            }
+              closeLine(nc);
+              hunk.incBoth();
+              lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
 
-            closeLine(nc);
+            } else if (hunk.isModifiedLine()) {
+              final boolean del = hunk.isDeletedA();
+              final boolean ins = hunk.isInsertedB();
+              final boolean full =
+                  intraline && hunk.getCurEdit().getType() != Edit.Type.REPLACE;
+              openLine(nc);
 
-            if (del && ins) {
-              lines.add(new PatchLine(REPLACE, hunk.getCurA(), hunk.getCurB()));
-            } else if (del) {
-              lines.add(new PatchLine(DELETE, hunk.getCurA(), -1));
-            } else if (ins) {
-              lines.add(new PatchLine(INSERT, -1, hunk.getCurB()));
+              if (del) {
+                appendLineNumber(nc, hunk.getCurA(), false);
+                appendLineText(nc, DELETE, a, hunk.getCurA(), full);
+                hunk.incA();
+              } else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
+                appendLineNumber(nc, false);
+                appendLineNone(nc, DELETE);
+              } else {
+                appendLineNumber(nc, false);
+                appendLineNone(nc, CONTEXT);
+              }
+
+              if (ins) {
+                appendLineText(nc, INSERT, b, hunk.getCurB(), full);
+                appendLineNumber(nc, hunk.getCurB(), true);
+                hunk.incB();
+              } else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
+                appendLineNone(nc, INSERT);
+                appendLineNumber(nc, true);
+              } else {
+                appendLineNone(nc, CONTEXT);
+                appendLineNumber(nc, true);
+              }
+
+              closeLine(nc);
+
+              if (del && ins) {
+                lines.add(new PatchLine(REPLACE, hunk.getCurA(), hunk.getCurB()));
+              } else if (del) {
+                lines.add(new PatchLine(DELETE, hunk.getCurA(), -1));
+              } else if (ins) {
+                lines.add(new PatchLine(INSERT, -1, hunk.getCurB()));
+              }
             }
           }
+          lastA = hunk.getCurA();
+          lastB = hunk.getCurB();
         }
-        lastA = hunk.getCurA();
-        lastB = hunk.getCurB();
-      }
-      if (lastB != b.size()) {
-        appendSkipLine(nc, b.size() - lastB);
-        lines.add(new SkippedLine(lastA, lastB, b.size() - lastB));
+        if (lastB != b.size()) {
+          appendSkipLine(nc, b.size() - lastB);
+          lines.add(new SkippedLine(lastA, lastB, b.size() - lastB));
+        }
       }
     }else{
       // Display the patch header for binary
@@ -190,14 +192,19 @@
         appendFileHeader(nc, line);
       }
     }
+    if (!hasDifferences(script)) {
+      appendNoDifferences(nc);
+    }
     resetHtml(nc);
     populateTableHeader(script, detail);
-    initScript(script);
-    if (!isDisplayBinary) {
-      for (int row = 0; row < lines.size(); row++) {
-        setRowItem(row, lines.get(row));
-        if (lines.get(row) instanceof SkippedLine) {
-          createSkipLine(row, (SkippedLine) lines.get(row));
+    if (hasDifferences(script)) {
+      initScript(script);
+      if (!isDisplayBinary) {
+        for (int row = 0; row < lines.size(); row++) {
+          setRowItem(row, lines.get(row));
+          if (lines.get(row) instanceof SkippedLine) {
+            createSkipLine(row, (SkippedLine) lines.get(row));
+          }
         }
       }
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
index c143acc..479ded7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
@@ -266,76 +266,83 @@
         nc.closeTr();
       }
 
-      final boolean syntaxHighlighting =
-          script.getDiffPrefs().isSyntaxHighlighting();
-      for (final EditList.Hunk hunk : script.getHunks()) {
-        appendHunkHeader(nc, hunk);
-        while (hunk.next()) {
-          if (hunk.isContextLine()) {
-            openLine(nc);
-            appendLineNumberForSideA(nc, hunk.getCurA());
-            appendLineNumberForSideB(nc, hunk.getCurB());
-            appendLineText(nc, false, CONTEXT, a, hunk.getCurA());
-            closeLine(nc);
-            hunk.incBoth();
-            lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
+      if (hasDifferences(script)) {
+        final boolean syntaxHighlighting =
+            script.getDiffPrefs().isSyntaxHighlighting();
+        for (final EditList.Hunk hunk : script.getHunks()) {
+          appendHunkHeader(nc, hunk);
+          while (hunk.next()) {
+            if (hunk.isContextLine()) {
+              openLine(nc);
+              appendLineNumberForSideA(nc, hunk.getCurA());
+              appendLineNumberForSideB(nc, hunk.getCurB());
+              appendLineText(nc, false, CONTEXT, a, hunk.getCurA());
+              closeLine(nc);
+              hunk.incBoth();
+              lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
 
-          } else if (hunk.isDeletedA()) {
-            openLine(nc);
-            appendLineNumberForSideA(nc, hunk.getCurA());
-            padLineNumberForSideB(nc);
-            appendLineText(nc, syntaxHighlighting, DELETE, a, hunk.getCurA());
-            closeLine(nc);
-            hunk.incA();
-            lines.add(new PatchLine(DELETE, hunk.getCurA(), -1));
-            if (a.size() == hunk.getCurA()
-                && script.getA().isMissingNewlineAtEnd()) {
-              appendNoLF(nc);
-            }
+            } else if (hunk.isDeletedA()) {
+              openLine(nc);
+              appendLineNumberForSideA(nc, hunk.getCurA());
+              padLineNumberForSideB(nc);
+              appendLineText(nc, syntaxHighlighting, DELETE, a, hunk.getCurA());
+              closeLine(nc);
+              hunk.incA();
+              lines.add(new PatchLine(DELETE, hunk.getCurA(), -1));
+              if (a.size() == hunk.getCurA()
+                  && script.getA().isMissingNewlineAtEnd()) {
+                appendNoLF(nc);
+              }
 
-          } else if (hunk.isInsertedB()) {
-            openLine(nc);
-            padLineNumberForSideA(nc);
-            appendLineNumberForSideB(nc, hunk.getCurB());
-            appendLineText(nc, syntaxHighlighting, INSERT, b, hunk.getCurB());
-            closeLine(nc);
-            hunk.incB();
-            lines.add(new PatchLine(INSERT, -1, hunk.getCurB()));
-            if (b.size() == hunk.getCurB()
-                && script.getB().isMissingNewlineAtEnd()) {
-              appendNoLF(nc);
+            } else if (hunk.isInsertedB()) {
+              openLine(nc);
+              padLineNumberForSideA(nc);
+              appendLineNumberForSideB(nc, hunk.getCurB());
+              appendLineText(nc, syntaxHighlighting, INSERT, b, hunk.getCurB());
+              closeLine(nc);
+              hunk.incB();
+              lines.add(new PatchLine(INSERT, -1, hunk.getCurB()));
+              if (b.size() == hunk.getCurB()
+                  && script.getB().isMissingNewlineAtEnd()) {
+                appendNoLF(nc);
+              }
             }
           }
         }
       }
     }
+    if (!hasDifferences(script)) {
+      appendNoDifferences(nc);
+    }
     resetHtml(nc);
     populateTableHeader(script, detail);
-    initScript(script);
-    if (!isDisplayBinary) {
-      int row = script.getPatchHeader().size();
-      final CellFormatter fmt = table.getCellFormatter();
-      final Iterator<PatchLine> iLine = lines.iterator();
-      while (iLine.hasNext()) {
-        final PatchLine l = iLine.next();
-        final String n;
-        switch (l.getType()) {
-          case CONTEXT:
-            n = Gerrit.RESOURCES.css().diffTextCONTEXT();
-            break;
-          case DELETE:
-            n = Gerrit.RESOURCES.css().diffTextDELETE();
-            break;
-          case INSERT:
-            n = Gerrit.RESOURCES.css().diffTextINSERT();
-            break;
-          default:
-            continue;
+    if (hasDifferences(script)) {
+      initScript(script);
+      if (!isDisplayBinary) {
+        int row = script.getPatchHeader().size();
+        final CellFormatter fmt = table.getCellFormatter();
+        final Iterator<PatchLine> iLine = lines.iterator();
+        while (iLine.hasNext()) {
+          final PatchLine l = iLine.next();
+          final String n;
+          switch (l.getType()) {
+            case CONTEXT:
+              n = Gerrit.RESOURCES.css().diffTextCONTEXT();
+              break;
+            case DELETE:
+              n = Gerrit.RESOURCES.css().diffTextDELETE();
+              break;
+            case INSERT:
+              n = Gerrit.RESOURCES.css().diffTextINSERT();
+              break;
+            default:
+              continue;
+          }
+          while (!fmt.getStyleName(row, PC).contains(n)) {
+            row++;
+          }
+          setRowItem(row++, l);
         }
-        while (!fmt.getStyleName(row, PC).contains(n)) {
-          row++;
-        }
-        setRowItem(row++, l);
       }
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FilteredUserInterface.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FilteredUserInterface.java
new file mode 100644
index 0000000..02244d0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FilteredUserInterface.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 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.ui;
+
+public interface FilteredUserInterface {
+  /**
+   * Return the value by which the user interface is currently filtered.
+   *
+   * @return value by which the user interface is currently filtered,
+   *         <code>null</code> or empty String if currently no filter is applied
+   */
+  public String getCurrentFilter();
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/IgnoreOutdatedFilterResultsCallbackWrapper.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/IgnoreOutdatedFilterResultsCallbackWrapper.java
new file mode 100644
index 0000000..c9cadcc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/IgnoreOutdatedFilterResultsCallbackWrapper.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2013 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.ui;
+
+import com.google.gerrit.client.rpc.GerritCallback;
+
+/**
+ * GerritCallback to be used on user interfaces that allow filtering to handle
+ * RPC's that request filtering. The user may change the filter quickly so that
+ * a response may be outdated when the client receives it. In this case the
+ * response must be ignored because the responses to RCP's may come out-of-order
+ * and an outdated response would overwrite the correct result which was
+ * received before.
+ */
+public class IgnoreOutdatedFilterResultsCallbackWrapper<T> extends GerritCallback<T> {
+  private final FilteredUserInterface filteredUI;
+  private final String myFilter;
+  private final GerritCallback<T> cb;
+
+  public IgnoreOutdatedFilterResultsCallbackWrapper(
+      final FilteredUserInterface filteredUI, final GerritCallback<T> cb) {
+    this.filteredUI = filteredUI;
+    this.myFilter = filteredUI.getCurrentFilter();
+    this.cb = cb;
+  }
+
+  @Override
+  public void onSuccess(final T result) {
+    if ((myFilter == null && filteredUI.getCurrentFilter() == null)
+        || (myFilter != null && myFilter.equals(filteredUI.getCurrentFilter()))) {
+      cb.onSuccess(result);
+    }
+    // Else ignore the result, the user has already changed the filter
+    // and the result is not relevant anymore. If multiple RPC's are
+    // fired the results may come back out-of-order and a non-relevant
+    // result could overwrite the correct result if not ignored.
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
index 9f0d6af..cac7667 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
@@ -36,7 +36,7 @@
 import com.google.gwtexpui.user.client.PluginSafeDialogBox;
 
 /** It creates a popup containing all the projects. */
-public class ProjectListPopup {
+public class ProjectListPopup implements FilteredUserInterface {
   private HighlightingProjectsTable projectsTab;
   private PluginSafeDialogBox popup;
   private NpTextBox filterTxt;
@@ -174,27 +174,22 @@
   }
 
   protected void populateProjects() {
-    final String mySubname = subname;
-    ProjectMap.match(subname, new GerritCallback<ProjectMap>() {
-      @Override
-      public void onSuccess(final ProjectMap result) {
-        if ((mySubname == null && subname == null)
-            || (mySubname != null && mySubname.equals(subname))) {
-          display(result);
-        }
-        // Else ignore the result, the user has already changed subname and
-        // the result is not relevant anymore. If multiple RPC's are fired
-        // the results may come back out-of-order and a non-relevant result
-        // could overwrite the correct result if not ignored.
-      }
-    });
+    ProjectMap.match(subname,
+        new IgnoreOutdatedFilterResultsCallbackWrapper<ProjectMap>(this,
+            new GerritCallback<ProjectMap>() {
+              @Override
+              public void onSuccess(final ProjectMap result) {
+                projectsTab.display(result, subname);
+                if (firstPopupLoad) { // Display was delayed until table was loaded
+                  firstPopupLoad = false;
+                  displayPopup();
+                }
+              }
+            }));
   }
 
-  private void display(final ProjectMap result) {
-    projectsTab.display(result, subname);
-    if (firstPopupLoad) { // Display was delayed until table was loaded
-      firstPopupLoad = false;
-      displayPopup();
-    }
+  @Override
+  public String getCurrentFilter() {
+    return subname;
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index 5667996..8d9d354 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -27,6 +27,7 @@
 import com.google.gerrit.httpd.rpc.account.AccountsRestApiServlet;
 import com.google.gerrit.httpd.rpc.change.ChangesRestApiServlet;
 import com.google.gerrit.httpd.rpc.change.DeprecatedChangeQueryServlet;
+import com.google.gerrit.httpd.rpc.group.GroupsRestApiServlet;
 import com.google.gerrit.httpd.rpc.project.ProjectsRestApiServlet;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
@@ -97,6 +98,7 @@
     filter("/a/*").through(RequireIdentifiedUserFilter.class);
     serveRegex("^/(?:a/)?accounts/(.*)$").with(AccountsRestApiServlet.class);
     serveRegex("^/(?:a/)?changes/(.*)$").with(ChangesRestApiServlet.class);
+    serveRegex("^/(?:a/)?groups/(.*)?$").with(GroupsRestApiServlet.class);
     serveRegex("^/(?:a/)?projects/(.*)?$").with(ProjectsRestApiServlet.class);
 
     if (cfg.deprecatedQuery) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
index 957f339..745c7ba 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
@@ -34,10 +34,8 @@
         factory(DeleteExternalIds.Factory.class);
         factory(ExternalIdDetailFactory.Factory.class);
         factory(GroupDetailHandler.Factory.class);
-        factory(MyGroupsFactory.Factory.class);
         factory(RegisterNewEmailSender.Factory.class);
         factory(RenameGroup.Factory.class);
-        factory(VisibleGroupsHandler.Factory.class);
       }
     });
     rpc(AccountSecurityImpl.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
index 72ad252..07d7690 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
@@ -22,7 +22,6 @@
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Account;
@@ -86,7 +85,6 @@
   private final ChangeUserName.CurrentUser changeUserNameFactory;
   private final DeleteExternalIds.Factory deleteExternalIdsFactory;
   private final ExternalIdDetailFactory.Factory externalIdDetailFactory;
-  private final MyGroupsFactory.Factory myGroupsFactory;
 
   private final ChangeHooks hooks;
   private final GroupCache groupCache;
@@ -104,7 +102,6 @@
       final ChangeUserName.CurrentUser changeUserNameFactory,
       final DeleteExternalIds.Factory deleteExternalIdsFactory,
       final ExternalIdDetailFactory.Factory externalIdDetailFactory,
-      final MyGroupsFactory.Factory myGroupsFactory,
       final ChangeHooks hooks, final GroupCache groupCache) {
     super(schema, currentUser);
     contactStore = cs;
@@ -126,7 +123,6 @@
     this.changeUserNameFactory = changeUserNameFactory;
     this.deleteExternalIdsFactory = deleteExternalIdsFactory;
     this.externalIdDetailFactory = externalIdDetailFactory;
-    this.myGroupsFactory = myGroupsFactory;
     this.hooks = hooks;
     this.groupCache = groupCache;
   }
@@ -211,16 +207,6 @@
     externalIdDetailFactory.create().to(callback);
   }
 
-  @Override
-  public void myGroups(final AsyncCallback<List<AccountGroup>> callback) {
-    run(callback, new Action<List<AccountGroup>>() {
-      public List<AccountGroup> run(final ReviewDb db) throws OrmException,
-          NoSuchGroupException, Failure {
-        return myGroupsFactory.create().call();
-      }
-    });
-  }
-
   public void deleteExternalIds(final Set<AccountExternalId.Key> keys,
       final AsyncCallback<Set<AccountExternalId.Key>> callback) {
     deleteExternalIdsFactory.create(keys).to(callback);
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 aca2e05..1651c2d 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
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.common.data.GroupAdminService;
 import com.google.gerrit.common.data.GroupDetail;
-import com.google.gerrit.common.data.GroupList;
 import com.google.gerrit.common.data.GroupOptions;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.InactiveAccountException;
@@ -27,8 +26,8 @@
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.client.AuthType;
@@ -69,7 +68,6 @@
   private final CreateGroup.Factory createGroupFactory;
   private final RenameGroup.Factory renameGroupFactory;
   private final GroupDetailHandler.Factory groupDetailFactory;
-  private final VisibleGroupsHandler.Factory visibleGroupsFactory;
 
   @Inject
   GroupAdminServiceImpl(final Provider<ReviewDb> schema,
@@ -84,8 +82,7 @@
       final GroupControl.Factory groupControlFactory,
       final CreateGroup.Factory createGroupFactory,
       final RenameGroup.Factory renameGroupFactory,
-      final GroupDetailHandler.Factory groupDetailFactory,
-      final VisibleGroupsHandler.Factory visibleGroupsFactory) {
+      final GroupDetailHandler.Factory groupDetailFactory) {
     super(schema, currentUser);
     this.accountCache = accountCache;
     this.groupIncludeCache = groupIncludeCache;
@@ -98,11 +95,6 @@
     this.createGroupFactory = createGroupFactory;
     this.renameGroupFactory = renameGroupFactory;
     this.groupDetailFactory = groupDetailFactory;
-    this.visibleGroupsFactory = visibleGroupsFactory;
-  }
-
-  public void visibleGroups(final AsyncCallback<GroupList> callback) {
-    visibleGroupsFactory.create().to(callback);
   }
 
   public void createGroup(final String newName,
@@ -225,7 +217,8 @@
   }
 
   public void addGroupInclude(final AccountGroup.Id groupId,
-      final String groupName, final AsyncCallback<GroupDetail> callback) {
+      final AccountGroup.UUID incGroupUUID, final String incGroupName,
+      final AsyncCallback<GroupDetail> callback) {
     run(callback, new Action<GroupDetail>() {
       public GroupDetail run(ReviewDb db) throws OrmException, Failure,
           NoSuchGroupException {
@@ -234,21 +227,24 @@
           throw new Failure(new NameAlreadyUsedException());
         }
 
-        final AccountGroup a = findGroup(groupName);
-        if (!control.canAddGroup(a.getId())) {
+        if (incGroupUUID == null) {
+          throw new Failure(new NoSuchGroupException(incGroupName));
+        }
+
+        if (!control.canAddGroup(incGroupUUID)) {
           throw new Failure(new NoSuchEntityException());
         }
 
-        final AccountGroupInclude.Key key =
-            new AccountGroupInclude.Key(groupId, a.getId());
-        AccountGroupInclude m = db.accountGroupIncludes().get(key);
+        final AccountGroupIncludeByUuid.Key key =
+            new AccountGroupIncludeByUuid.Key(groupId, incGroupUUID);
+        AccountGroupIncludeByUuid m = db.accountGroupIncludesByUuid().get(key);
         if (m == null) {
-          m = new AccountGroupInclude(key);
-          db.accountGroupIncludesAudit().insert(
-              Collections.singleton(new AccountGroupIncludeAudit(m,
+          m = new AccountGroupIncludeByUuid(key);
+          db.accountGroupIncludesByUuidAudit().insert(
+              Collections.singleton(new AccountGroupIncludeByUuidAudit(m,
                   getAccountId())));
-          db.accountGroupIncludes().insert(Collections.singleton(m));
-          groupIncludeCache.evictInclude(a.getGroupUUID());
+          db.accountGroupIncludesByUuid().insert(Collections.singleton(m));
+          groupIncludeCache.evictInclude(incGroupUUID);
         }
 
         return groupDetailFactory.create(groupId).call();
@@ -311,7 +307,7 @@
   }
 
   public void deleteGroupIncludes(final AccountGroup.Id groupId,
-      final Set<AccountGroupInclude.Key> keys,
+      final Set<AccountGroupIncludeByUuid.Key> keys,
       final AsyncCallback<VoidResult> callback) {
     run(callback, new Action<VoidResult>() {
       public VoidResult run(final ReviewDb db) throws OrmException,
@@ -321,26 +317,26 @@
           throw new Failure(new NameAlreadyUsedException());
         }
 
-        for (final AccountGroupInclude.Key k : keys) {
+        for (final AccountGroupIncludeByUuid.Key k : keys) {
           if (!groupId.equals(k.getGroupId())) {
             throw new Failure(new NoSuchEntityException());
           }
         }
 
         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);
+        final Set<AccountGroup.UUID> groupsToEvict = new HashSet<AccountGroup.UUID>();
+        for (final AccountGroupIncludeByUuid.Key k : keys) {
+          final AccountGroupIncludeByUuid m =
+              db.accountGroupIncludesByUuid().get(k);
           if (m != null) {
-            if (!control.canRemoveGroup(m.getIncludeId())) {
+            if (!control.canRemoveGroup(m.getIncludeUUID())) {
               throw new Failure(new NoSuchEntityException());
             }
 
-            AccountGroupIncludeAudit audit = null;
-            for (AccountGroupIncludeAudit a : db
-                .accountGroupIncludesAudit().byGroupInclude(
-                    m.getGroupId(), m.getIncludeId())) {
+            AccountGroupIncludeByUuidAudit audit = null;
+            for (AccountGroupIncludeByUuidAudit a : db
+                .accountGroupIncludesByUuidAudit().byGroupInclude(
+                    m.getGroupId(), m.getIncludeUUID())) {
               if (a.isActive()) {
                 audit = a;
                 break;
@@ -349,15 +345,15 @@
 
             if (audit != null) {
               audit.removed(me);
-              db.accountGroupIncludesAudit().update(
+              db.accountGroupIncludesByUuidAudit().update(
                   Collections.singleton(audit));
             }
-            db.accountGroupIncludes().delete(Collections.singleton(m));
-            groupsToEvict.add(k.getIncludeId());
+            db.accountGroupIncludesByUuid().delete(Collections.singleton(m));
+            groupsToEvict.add(k.getIncludeUUID());
           }
         }
-        for (AccountGroup group : db.accountGroups().get(groupsToEvict)) {
-          groupIncludeCache.evictInclude(group.getGroupUUID());
+        for (AccountGroup.UUID uuid : groupsToEvict) {
+          groupIncludeCache.evictInclude(uuid);
         }
         return VoidResult.INSTANCE;
       }
@@ -408,14 +404,4 @@
       return null;
     }
   }
-
-  private AccountGroup findGroup(final String name) throws OrmException,
-      Failure {
-    final AccountGroup g = groupCache.get(new AccountGroup.NameKey(name));
-    if (g == null) {
-      throw new Failure(new NoSuchGroupException(name));
-    }
-    return g;
-  }
-
 }
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
deleted file mode 100644
index 33ce371..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java
+++ /dev/null
@@ -1,46 +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.httpd.rpc.account;
-
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.VisibleGroups;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-
-import java.util.List;
-
-class MyGroupsFactory extends Handler<List<AccountGroup>> {
-  interface Factory {
-    MyGroupsFactory create();
-  }
-
-  private final VisibleGroups.Factory visibleGroupsFactory;
-  private final IdentifiedUser user;
-
-  @Inject
-  MyGroupsFactory(final VisibleGroups.Factory visibleGroupsFactory, final IdentifiedUser user) {
-    this.visibleGroupsFactory = visibleGroupsFactory;
-    this.user = user;
-  }
-
-  @Override
-  public List<AccountGroup> call() throws OrmException, NoSuchGroupException {
-    final VisibleGroups visibleGroups = visibleGroupsFactory.create();
-    return visibleGroups.get(user).getGroups();
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/VisibleGroupsHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/VisibleGroupsHandler.java
deleted file mode 100644
index 54f91f7..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/VisibleGroupsHandler.java
+++ /dev/null
@@ -1,39 +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.httpd.rpc.account;
-
-import com.google.gerrit.common.data.GroupList;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.server.account.VisibleGroups;
-import com.google.inject.Inject;
-
-public class VisibleGroupsHandler extends Handler<GroupList> {
-
-  interface Factory {
-    VisibleGroupsHandler create();
-  }
-
-  private final VisibleGroups.Factory visibleGroupsFactory;
-
-  @Inject
-  VisibleGroupsHandler(final VisibleGroups.Factory visibleGroupsFactory) {
-    this.visibleGroupsFactory = visibleGroupsFactory;
-  }
-
-  @Override
-  public GroupList call() throws Exception {
-    return visibleGroupsFactory.create().get();
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
index 1ac6912..c38bda7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
@@ -28,18 +28,22 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.mail.CommitMessageEditedSender;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
 
@@ -63,6 +67,7 @@
   private final String message;
 
   private final ChangeHooks hooks;
+  private final CommitValidators.Factory commitValidatorsFactory;
 
   private final GitRepositoryManager gitManager;
   private final PatchSetInfoFactory patchSetInfoFactory;
@@ -76,6 +81,7 @@
       final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory,
       @Assisted final PatchSet.Id patchSetId,
       @Assisted @Nullable final String message, final ChangeHooks hooks,
+      final CommitValidators.Factory commitValidatorsFactory,
       final GitRepositoryManager gitManager,
       final PatchSetInfoFactory patchSetInfoFactory,
       final GitReferenceUpdated replication,
@@ -89,6 +95,7 @@
     this.patchSetId = patchSetId;
     this.message = message;
     this.hooks = hooks;
+    this.commitValidatorsFactory = commitValidatorsFactory;
     this.gitManager = gitManager;
 
     this.patchSetInfoFactory = patchSetInfoFactory;
@@ -109,10 +116,22 @@
           "Not allowed to add new Patch Sets to: " + changeId.toString());
     }
 
-    ChangeUtil.editCommitMessage(patchSetId, currentUser, message, db,
-        commitMessageEditedSenderFactory, hooks, gitManager, patchSetInfoFactory,
-        replication, myIdent);
+    final Repository git;
+    try {
+      git = gitManager.openRepository(db.changes().get(changeId).getProject());
+    } catch (RepositoryNotFoundException e) {
+      throw new NoSuchChangeException(changeId, e);
+    }
+    try {
+      CommitValidators commitValidators =
+          commitValidatorsFactory.create(control.getRefControl(), new NoSshInfo(), git);
 
-    return changeDetailFactory.create(changeId).call();
+      ChangeUtil.editCommitMessage(patchSetId, control.getRefControl(), commitValidators, currentUser, message, db,
+          commitMessageEditedSenderFactory, hooks, git, patchSetInfoFactory, replication, myIdent);
+
+      return changeDetailFactory.create(changeId).call();
+    } finally {
+      git.close();
+    }
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/group/GroupsRestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/group/GroupsRestApiServlet.java
new file mode 100644
index 0000000..04dc747
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/group/GroupsRestApiServlet.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2013 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.group;
+
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class GroupsRestApiServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+
+  @Inject
+  GroupsRestApiServlet(RestApiServlet.Globals globals,
+      Provider<GroupsCollection> groups) {
+    super(globals, groups);
+  }
+}
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
index 7b3b8e7..ea95e96 100644
--- 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
@@ -121,6 +121,9 @@
         if (pc.isOwner()) {
           local.add(section);
           ownerOf.add(name);
+
+        } else if (metaConfigControl.isVisible()) {
+          local.add(section);
         }
 
       } else if (RefConfigSection.isValid(name)) {
diff --git a/gerrit-package-plugins/pom.xml b/gerrit-package-plugins/pom.xml
index 9f11bb3..5a4cdbb 100644
--- a/gerrit-package-plugins/pom.xml
+++ b/gerrit-package-plugins/pom.xml
@@ -43,7 +43,7 @@
     <dependency>
       <groupId>com.googlesource.gerrit.plugins.replication</groupId>
       <artifactId>replication</artifactId>
-      <version>1.0-rc0</version>
+      <version>1.0</version>
       <scope>provided</scope>
     </dependency>
   </dependencies>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupInclude.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuid.java
similarity index 71%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupInclude.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuid.java
index 4b58689..a5b35ed 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupInclude.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuid.java
@@ -18,7 +18,7 @@
 import com.google.gwtorm.client.CompoundKey;
 
 /** Membership of an {@link AccountGroup} in an {@link AccountGroup}. */
-public final class AccountGroupInclude {
+public final class AccountGroupIncludeByUuid {
   public static class Key extends CompoundKey<AccountGroup.Id> {
     private static final long serialVersionUID = 1L;
 
@@ -26,16 +26,16 @@
     protected AccountGroup.Id groupId;
 
     @Column(id = 2)
-    protected AccountGroup.Id includeId;
+    protected AccountGroup.UUID includeUUID;
 
     protected Key() {
       groupId = new AccountGroup.Id();
-      includeId = new AccountGroup.Id();
+      includeUUID = new AccountGroup.UUID();
     }
 
-    public Key(final AccountGroup.Id g, final AccountGroup.Id i) {
+    public Key(final AccountGroup.Id g, final AccountGroup.UUID u) {
       groupId = g;
-      includeId = i;
+      includeUUID = u;
     }
 
     @Override
@@ -47,27 +47,27 @@
       return groupId;
     }
 
-    public AccountGroup.Id getIncludeId() {
-      return includeId;
+    public AccountGroup.UUID getIncludeUUID() {
+      return includeUUID;
     }
 
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {includeId};
+      return new com.google.gwtorm.client.Key<?>[] {includeUUID};
     }
   }
 
   @Column(id = 1, name = Column.NONE)
   protected Key key;
 
-  protected AccountGroupInclude() {
+  protected AccountGroupIncludeByUuid() {
   }
 
-  public AccountGroupInclude(final AccountGroupInclude.Key k) {
+  public AccountGroupIncludeByUuid(final AccountGroupIncludeByUuid.Key k) {
     key = k;
   }
 
-  public AccountGroupInclude.Key getKey() {
+  public AccountGroupIncludeByUuid.Key getKey() {
     return key;
   }
 
@@ -75,7 +75,7 @@
     return key.groupId;
   }
 
-  public AccountGroup.Id getIncludeId() {
-    return key.includeId;
+  public AccountGroup.UUID getIncludeUUID() {
+    return key.includeUUID;
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuidAudit.java
similarity index 67%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeAudit.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuidAudit.java
index 275c3c3..6625197 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeAudit.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuidAudit.java
@@ -20,7 +20,7 @@
 import java.sql.Timestamp;
 
 /** Inclusion of an {@link AccountGroup} in another {@link AccountGroup}. */
-public final class AccountGroupIncludeAudit {
+public final class AccountGroupIncludeByUuidAudit {
   public static class Key extends CompoundKey<AccountGroup.Id> {
     private static final long serialVersionUID = 1L;
 
@@ -28,19 +28,19 @@
     protected AccountGroup.Id groupId;
 
     @Column(id = 2)
-    protected AccountGroup.Id includeId;
+    protected AccountGroup.UUID includeUUID;
 
     @Column(id = 3)
     protected Timestamp addedOn;
 
     protected Key() {
       groupId = new AccountGroup.Id();
-      includeId = new AccountGroup.Id();
+      includeUUID = new AccountGroup.UUID();
     }
 
-    public Key(final AccountGroup.Id g, final AccountGroup.Id i, final Timestamp t) {
+    public Key(final AccountGroup.Id g, final AccountGroup.UUID u, final Timestamp t) {
       groupId = g;
-      includeId = i;
+      includeUUID = u;
       addedOn = t;
     }
 
@@ -49,8 +49,8 @@
       return groupId;
     }
 
-    public AccountGroup.Id getIncludedId() {
-      return includeId;
+    public AccountGroup.UUID getIncludeUUID() {
+      return includeUUID;
     }
 
     public Timestamp getAddedOn() {
@@ -59,7 +59,7 @@
 
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {includeId};
+      return new com.google.gwtorm.client.Key<?>[] {includeUUID};
     }
   }
 
@@ -75,18 +75,23 @@
   @Column(id = 4, notNull = false)
   protected Timestamp removedOn;
 
-  protected AccountGroupIncludeAudit() {
+  protected AccountGroupIncludeByUuidAudit() {
   }
 
-  public AccountGroupIncludeAudit(final AccountGroupInclude m,
-      final Account.Id adder) {
+  public AccountGroupIncludeByUuidAudit(final AccountGroupIncludeByUuid m,
+      final Account.Id adder, final Timestamp when) {
     final AccountGroup.Id group = m.getGroupId();
-    final AccountGroup.Id include = m.getIncludeId();
-    key = new AccountGroupIncludeAudit.Key(group, include, now());
+    final AccountGroup.UUID include = m.getIncludeUUID();
+    key = new AccountGroupIncludeByUuidAudit.Key(group, include, when);
     addedBy = adder;
   }
 
-  public AccountGroupIncludeAudit.Key getKey() {
+  public AccountGroupIncludeByUuidAudit(final AccountGroupIncludeByUuid m,
+      final Account.Id adder) {
+    this(m, adder, now());
+  }
+
+  public AccountGroupIncludeByUuidAudit.Key getKey() {
     return key;
   }
 
@@ -99,7 +104,12 @@
     removedOn = now();
   }
 
+  public void removed(final Account.Id deleter, final Timestamp when) {
+    removedBy = deleter;
+    removedOn = when;
+  }
+
   private static Timestamp now() {
     return new Timestamp(System.currentTimeMillis());
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAccess.java
similarity index 65%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAccess.java
index 3ee4ba0..78a4128 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAccess.java
@@ -15,21 +15,21 @@
 package com.google.gerrit.reviewdb.server;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
 import com.google.gwtorm.server.Query;
 import com.google.gwtorm.server.ResultSet;
 
-public interface AccountGroupIncludeAccess extends
-    Access<AccountGroupInclude, AccountGroupInclude.Key> {
+public interface AccountGroupIncludeByUuidAccess extends
+    Access<AccountGroupIncludeByUuid, AccountGroupIncludeByUuid.Key> {
   @PrimaryKey("key")
-  AccountGroupInclude get(AccountGroupInclude.Key key) throws OrmException;
+  AccountGroupIncludeByUuid get(AccountGroupIncludeByUuid.Key key) throws OrmException;
 
-  @Query("WHERE key.includeId = ?")
-  ResultSet<AccountGroupInclude> byInclude(AccountGroup.Id id) throws OrmException;
+  @Query("WHERE key.includeUUID = ?")
+  ResultSet<AccountGroupIncludeByUuid> byIncludeUUID(AccountGroup.UUID uuid) throws OrmException;
 
   @Query("WHERE key.groupId = ?")
-  ResultSet<AccountGroupInclude> byGroup(AccountGroup.Id id) throws OrmException;
+  ResultSet<AccountGroupIncludeByUuid> byGroup(AccountGroup.Id id) throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAuditAccess.java
similarity index 66%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAuditAccess.java
index b3f4f88..1c95f75 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAuditAccess.java
@@ -15,20 +15,20 @@
 package com.google.gerrit.reviewdb.server;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
 import com.google.gwtorm.server.Query;
 import com.google.gwtorm.server.ResultSet;
 
-public interface AccountGroupIncludeAuditAccess extends
-    Access<AccountGroupIncludeAudit, AccountGroupIncludeAudit.Key> {
+public interface AccountGroupIncludeByUuidAuditAccess extends
+    Access<AccountGroupIncludeByUuidAudit, AccountGroupIncludeByUuidAudit.Key> {
   @PrimaryKey("key")
-  AccountGroupIncludeAudit get(AccountGroupIncludeAudit.Key key)
+  AccountGroupIncludeByUuidAudit get(AccountGroupIncludeByUuidAudit.Key key)
       throws OrmException;
 
-  @Query("WHERE key.groupId = ? AND key.includeId = ?")
-  ResultSet<AccountGroupIncludeAudit> byGroupInclude(AccountGroup.Id groupId,
-      AccountGroup.Id incGroupId) throws OrmException;
+  @Query("WHERE key.groupId = ? AND key.includeUUID = ?")
+  ResultSet<AccountGroupIncludeByUuidAudit> byGroupInclude(AccountGroup.Id groupId,
+      AccountGroup.UUID incGroupUUID) throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 149626b..87bbd5b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -70,12 +70,6 @@
   @Relation(id = 13)
   AccountGroupMemberAuditAccess accountGroupMembersAudit();
 
-  @Relation(id = 14)
-  AccountGroupIncludeAccess accountGroupIncludes();
-
-  @Relation(id = 15)
-  AccountGroupIncludeAuditAccess accountGroupIncludesAudit();
-
   @Relation(id = 17)
   AccountDiffPreferenceAccess accountDiffPreferences();
 
@@ -112,6 +106,12 @@
   @Relation(id = 28)
   SubmoduleSubscriptionAccess submoduleSubscriptions();
 
+  @Relation(id = 29)
+  AccountGroupIncludeByUuidAccess accountGroupIncludesByUuid();
+
+  @Relation(id = 30)
+  AccountGroupIncludeByUuidAuditAccess accountGroupIncludesByUuidAudit();
+
   /** Create the next unique id for an {@link Account}. */
   @Sequence(startWith = 1000000)
   int nextAccountId() throws OrmException;
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index 9d453fc..d6609e7 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -34,10 +34,10 @@
 
 
 -- *********************************************************************
--- AccountGroupIncludeAccess
+-- AccountGroupIncludeByUuidAccess
 --    @PrimaryKey covers: byGroup
-CREATE INDEX account_group_includes_byInclude
-ON account_group_includes (include_id);
+CREATE INDEX account_group_includes_by_uuid_byInclude
+ON account_group_includes_by_uuid (include_uuid);
 
 
 -- *********************************************************************
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index 675c5bc..3b62e84 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -82,10 +82,10 @@
 
 
 -- *********************************************************************
--- AccountGroupIncludeAccess
+-- AccountGroupIncludeByUuidAccess
 --    @PrimaryKey covers: byGroup
-CREATE INDEX account_group_includes_byInclude
-ON account_group_includes (include_id);
+CREATE INDEX account_group_includes_by_uuid_byInclude
+ON account_group_includes_by_uuid (include_uuid);
 
 
 -- *********************************************************************
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 1f53cbd..ede8e74 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -27,15 +27,19 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.TrackingFooter;
 import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeOp;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.CommitMessageEditedSender;
 import com.google.gerrit.server.mail.RevertedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.RefControl;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmConcurrencyException;
@@ -43,7 +47,6 @@
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -54,6 +57,7 @@
 import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.Base64;
 import org.eclipse.jgit.util.ChangeIdUtil;
 import org.eclipse.jgit.util.NB;
@@ -187,14 +191,17 @@
     db.patchSetAncestors().insert(toInsert);
   }
 
-  public static Change.Id revert(final PatchSet.Id patchSetId,
-      final IdentifiedUser user, final String message, final ReviewDb db,
+  public static Change.Id revert(final RefControl refControl,
+      final PatchSet.Id patchSetId, final IdentifiedUser user,
+      final CommitValidators commitValidators,
+      final String message, final ReviewDb db,
       final RevertedSender.Factory revertedSenderFactory,
-      final ChangeHooks hooks, GitRepositoryManager gitManager,
+      final ChangeHooks hooks, Repository git,
       final PatchSetInfoFactory patchSetInfoFactory,
-      final GitReferenceUpdated replication, PersonIdent myIdent)
-      throws NoSuchChangeException, EmailException, OrmException,
-      MissingObjectException, IncorrectObjectTypeException, IOException {
+      final GitReferenceUpdated replication, PersonIdent myIdent,
+      String canonicalWebUrl) throws NoSuchChangeException, EmailException,
+      OrmException, MissingObjectException, IncorrectObjectTypeException,
+      IOException, InvalidChangeOperationException {
     final Change.Id changeId = patchSetId.getParentKey();
     final PatchSet patch = db.patchSets().get(patchSetId);
     if (patch == null) {
@@ -202,13 +209,6 @@
     }
     final Change changeToRevert = db.changes().get(changeId);
 
-    final Repository git;
-    try {
-      git = gitManager.openRepository(changeToRevert.getProject());
-    } catch (RepositoryNotFoundException e) {
-      throw new NoSuchChangeException(changeId, e);
-    }
-
     final RevWalk revWalk = new RevWalk(git);
     try {
       RevCommit commitToRevert =
@@ -255,6 +255,18 @@
       ps.setUploader(change.getOwner());
       ps.setRevision(new RevId(revertCommit.name()));
 
+      CommitReceivedEvent commitReceivedEvent =
+          new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(),
+              revertCommit.getId(), ps.getRefName()), refControl
+              .getProjectControl().getProject(), refControl.getRefName(),
+              revertCommit, user);
+
+      try {
+        commitValidators.validateForRevertCommits(commitReceivedEvent);
+      } catch (CommitValidationException e) {
+        throw new InvalidChangeOperationException(e.getMessage());
+      }
+
       change.setCurrentPatchSet(patchSetInfoFactory.get(revertCommit, ps.getId()));
       ChangeUtil.updated(change);
 
@@ -300,14 +312,14 @@
       return change.getId();
     } finally {
       revWalk.release();
-      git.close();
     }
   }
 
   public static Change.Id editCommitMessage(final PatchSet.Id patchSetId,
+      final RefControl refControl, CommitValidators commitValidators,
       final IdentifiedUser user, final String message, final ReviewDb db,
       final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory,
-      final ChangeHooks hooks, GitRepositoryManager gitManager,
+      final ChangeHooks hooks, Repository git,
       final PatchSetInfoFactory patchSetInfoFactory,
       final GitReferenceUpdated replication, PersonIdent myIdent)
       throws NoSuchChangeException, EmailException, OrmException,
@@ -323,128 +335,129 @@
       throw new InvalidChangeOperationException("The commit message cannot be empty");
     }
 
-    final Repository git;
+    final RevWalk revWalk = new RevWalk(git);
     try {
-      git = gitManager.openRepository(db.changes().get(changeId).getProject());
-    } catch (RepositoryNotFoundException e) {
-      throw new NoSuchChangeException(changeId, e);
-    }
+      RevCommit commit =
+          revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
+      if (commit.getFullMessage().equals(message)) {
+        throw new InvalidChangeOperationException("New commit message cannot be same as existing commit message");
+      }
 
-    try {
-      final RevWalk revWalk = new RevWalk(git);
+      Date now = myIdent.getWhen();
+      Change change = db.changes().get(changeId);
+      PersonIdent authorIdent =
+          user.newCommitterIdent(now, myIdent.getTimeZone());
+
+      CommitBuilder commitBuilder = new CommitBuilder();
+      commitBuilder.setTreeId(commit.getTree());
+      commitBuilder.setParentIds(commit.getParents());
+      commitBuilder.setAuthor(commit.getAuthorIdent());
+      commitBuilder.setCommitter(authorIdent);
+      commitBuilder.setMessage(message);
+
+      RevCommit newCommit;
+      final ObjectInserter oi = git.newObjectInserter();
       try {
-        RevCommit commit =
-            revWalk.parseCommit(ObjectId.fromString(patch.getRevision().get()));
-        if (commit.getFullMessage().equals(message)) {
-          throw new InvalidChangeOperationException("New commit message cannot be same as existing commit message");
+        ObjectId id = oi.insert(commitBuilder);
+        oi.flush();
+        newCommit = revWalk.parseCommit(id);
+      } finally {
+        oi.release();
+      }
+
+      final PatchSet originalPS = db.patchSets().get(patchSetId);
+      PatchSet.Id id = nextPatchSetId(git, change.currentPatchSetId());
+      final PatchSet newPatchSet = new PatchSet(id);
+      newPatchSet.setCreatedOn(new Timestamp(now.getTime()));
+      newPatchSet.setUploader(user.getAccountId());
+      newPatchSet.setRevision(new RevId(newCommit.name()));
+      newPatchSet.setDraft(originalPS.isDraft());
+
+      final PatchSetInfo info =
+          patchSetInfoFactory.get(newCommit, newPatchSet.getId());
+
+      CommitReceivedEvent commitReceivedEvent =
+          new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(),
+              newCommit.getId(), newPatchSet.getRefName()), refControl
+              .getProjectControl().getProject(), refControl.getRefName(),
+              newCommit, user);
+
+      try {
+        commitValidators.validateForReceiveCommits(commitReceivedEvent);
+      } catch (CommitValidationException e) {
+        throw new InvalidChangeOperationException(e.getMessage());
+      }
+
+      final RefUpdate ru = git.updateRef(newPatchSet.getRefName());
+      ru.setExpectedOldObjectId(ObjectId.zeroId());
+      ru.setNewObjectId(newCommit);
+      ru.disableRefLog();
+      if (ru.update(revWalk) != RefUpdate.Result.NEW) {
+        throw new IOException(String.format(
+            "Failed to create ref %s in %s: %s", newPatchSet.getRefName(),
+            change.getDest().getParentKey().get(), ru.getResult()));
+      }
+      replication.fire(change.getProject(), ru.getName());
+
+      db.changes().beginTransaction(change.getId());
+      try {
+        Change updatedChange = db.changes().get(change.getId());
+        if (updatedChange != null && updatedChange.getStatus().isOpen()) {
+          change = updatedChange;
+        } else {
+          throw new InvalidChangeOperationException(String.format(
+              "Change %s is closed", change.getId()));
         }
 
-        Date now = myIdent.getWhen();
-        Change change = db.changes().get(changeId);
-        PersonIdent authorIdent =
-            user.newCommitterIdent(now, myIdent.getTimeZone());
-
-        CommitBuilder commitBuilder = new CommitBuilder();
-        commitBuilder.setTreeId(commit.getTree());
-        commitBuilder.setParentIds(commit.getParents());
-        commitBuilder.setAuthor(commit.getAuthorIdent());
-        commitBuilder.setCommitter(authorIdent);
-        commitBuilder.setMessage(message);
-
-        RevCommit newCommit;
-        final ObjectInserter oi = git.newObjectInserter();
-        try {
-          ObjectId id = oi.insert(commitBuilder);
-          oi.flush();
-          newCommit = revWalk.parseCommit(id);
-        } finally {
-          oi.release();
-        }
-
-        final PatchSet originalPS = db.patchSets().get(patchSetId);
-        PatchSet.Id id = nextPatchSetId(git, change.currentPatchSetId());
-        final PatchSet newPatchSet = new PatchSet(id);
-        newPatchSet.setCreatedOn(new Timestamp(now.getTime()));
-        newPatchSet.setUploader(user.getAccountId());
-        newPatchSet.setRevision(new RevId(newCommit.name()));
-        newPatchSet.setDraft(originalPS.isDraft());
-
-        final PatchSetInfo info =
-            patchSetInfoFactory.get(newCommit, newPatchSet.getId());
-
-        final RefUpdate ru = git.updateRef(newPatchSet.getRefName());
-        ru.setExpectedOldObjectId(ObjectId.zeroId());
-        ru.setNewObjectId(newCommit);
-        ru.disableRefLog();
-        if (ru.update(revWalk) != RefUpdate.Result.NEW) {
-          throw new IOException(String.format(
-              "Failed to create ref %s in %s: %s", newPatchSet.getRefName(),
-              change.getDest().getParentKey().get(), ru.getResult()));
-        }
-        replication.fire(change.getProject(), ru.getName());
-
-        db.changes().beginTransaction(change.getId());
-        try {
-          Change updatedChange = db.changes().get(change.getId());
-          if (updatedChange != null && updatedChange.getStatus().isOpen()) {
-            change = updatedChange;
-          } else {
-            throw new InvalidChangeOperationException(String.format(
-                "Change %s is closed", change.getId()));
-          }
-
-          ChangeUtil.insertAncestors(db, newPatchSet.getId(), commit);
-          db.patchSets().insert(Collections.singleton(newPatchSet));
-          updatedChange =
-              db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
-                @Override
-                public Change update(Change change) {
-                  if (change.getStatus().isClosed()) {
-                    return null;
-                  }
-                  if (!change.currentPatchSetId().equals(patchSetId)) {
-                    return null;
-                  }
-                  if (change.getStatus() != Change.Status.DRAFT) {
-                    change.setStatus(Change.Status.NEW);
-                  }
-                  change.setLastSha1MergeTested(null);
-                  change.setCurrentPatchSet(info);
-                  ChangeUtil.updated(change);
-                  return change;
+        ChangeUtil.insertAncestors(db, newPatchSet.getId(), commit);
+        db.patchSets().insert(Collections.singleton(newPatchSet));
+        updatedChange =
+            db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
+              @Override
+              public Change update(Change change) {
+                if (change.getStatus().isClosed()) {
+                  return null;
                 }
-              });
-          if (updatedChange != null) {
-            change = updatedChange;
-          } else {
-            throw new InvalidChangeOperationException(String.format(
-                "Change %s was modified", change.getId()));
-          }
+                if (!change.currentPatchSetId().equals(patchSetId)) {
+                  return null;
+                }
+                if (change.getStatus() != Change.Status.DRAFT) {
+                  change.setStatus(Change.Status.NEW);
+                }
+                change.setLastSha1MergeTested(null);
+                change.setCurrentPatchSet(info);
+                ChangeUtil.updated(change);
+                return change;
+              }
+            });
+        if (updatedChange != null) {
+          change = updatedChange;
+        } else {
+          throw new InvalidChangeOperationException(String.format(
+              "Change %s was modified", change.getId()));
+        }
 
-          final ChangeMessage cmsg =
-              new ChangeMessage(new ChangeMessage.Key(changeId,
-                  ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
-          final String msg = "Patch Set " + newPatchSet.getPatchSetId() + ": Commit message was updated";
-          cmsg.setMessage(msg);
-          db.changeMessages().insert(Collections.singleton(cmsg));
-          db.commit();
+        final ChangeMessage cmsg =
+            new ChangeMessage(new ChangeMessage.Key(changeId,
+                ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
+        final String msg = "Patch Set " + newPatchSet.getPatchSetId() + ": Commit message was updated";
+        cmsg.setMessage(msg);
+        db.changeMessages().insert(Collections.singleton(cmsg));
+        db.commit();
 
           final CommitMessageEditedSender cm = commitMessageEditedSenderFactory.create(change);
           cm.setFrom(user.getAccountId());
           cm.setChangeMessage(cmsg);
           cm.send();
-        } finally {
-          db.rollback();
-        }
-
-        hooks.doPatchsetCreatedHook(change, newPatchSet, db);
-
-        return change.getId();
       } finally {
-        revWalk.release();
+        db.rollback();
       }
+
+      hooks.doPatchsetCreatedHook(change, newPatchSet, db);
+
+      return change.getId();
     } finally {
-      git.close();
+      revWalk.release();
     }
   }
 
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 d9b12ac..06a3eba 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
@@ -84,6 +84,10 @@
     this(who, GroupDescriptions.forAccountGroup(ag));
   }
 
+  public GroupDescription.Basic getGroup() {
+    return group;
+  }
+
   public CurrentUser getCurrentUser() {
     return user;
   }
@@ -123,15 +127,15 @@
     return canSeeMembers();
   }
 
-  public boolean canAddGroup(AccountGroup.Id id) {
+  public boolean canAddGroup(AccountGroup.UUID uuid) {
     return isOwner();
   }
 
-  public boolean canRemoveGroup(AccountGroup.Id id) {
+  public boolean canRemoveGroup(AccountGroup.UUID uuid) {
     return isOwner();
   }
 
-  public boolean canSeeGroup(AccountGroup.Id id) {
+  public boolean canSeeGroup(AccountGroup.UUID uuid) {
     return canSeeMembers();
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
index 0aef40c..4d2cc1a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.OrmException;
@@ -123,21 +123,21 @@
     return members;
   }
 
-  private List<AccountGroupInclude> loadIncludes() throws OrmException {
-    List<AccountGroupInclude> groups = new ArrayList<AccountGroupInclude>();
+  private List<AccountGroupIncludeByUuid> loadIncludes() throws OrmException {
+    List<AccountGroupIncludeByUuid> groups = new ArrayList<AccountGroupIncludeByUuid>();
 
-    for (final AccountGroupInclude m : db.accountGroupIncludes().byGroup(groupId)) {
-      if (control.canSeeGroup(m.getIncludeId())) {
-        gic.want(m.getIncludeId());
+    for (final AccountGroupIncludeByUuid m : db.accountGroupIncludesByUuid().byGroup(groupId)) {
+      if (control.canSeeGroup(m.getIncludeUUID())) {
+        gic.want(m.getIncludeUUID());
         groups.add(m);
       }
     }
 
-    Collections.sort(groups, new Comparator<AccountGroupInclude>() {
-      public int compare(final AccountGroupInclude o1,
-          final AccountGroupInclude o2) {
-        final AccountGroup a = gic.get(o1.getIncludeId());
-        final AccountGroup b = gic.get(o2.getIncludeId());
+    Collections.sort(groups, new Comparator<AccountGroupIncludeByUuid>() {
+      public int compare(final AccountGroupIncludeByUuid o1,
+          final AccountGroupIncludeByUuid o2) {
+        final AccountGroup a = gic.get(o1.getIncludeUUID());
+        final AccountGroup b = gic.get(o2.getIncludeUUID());
         return n(a).compareTo(n(b));
       }
 
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 7fbba45..ba78210 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
@@ -19,7 +19,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gwtorm.server.SchemaFactory;
@@ -102,8 +102,8 @@
         }
 
         Set<AccountGroup.Id> ids = Sets.newHashSet();
-        for (AccountGroupInclude agi : db.accountGroupIncludes()
-            .byInclude(group.get(0).getId())) {
+        for (AccountGroupIncludeByUuid agi : db.accountGroupIncludesByUuid()
+            .byIncludeUUID(group.get(0).getGroupUUID())) {
           ids.add(agi.getGroupId());
         }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupInfoCacheFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupInfoCacheFactory.java
index 19d953d..4c399c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupInfoCacheFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupInfoCacheFactory.java
@@ -31,35 +31,35 @@
   }
 
   private final GroupCache groupCache;
-  private final Map<AccountGroup.Id, AccountGroup> out;
+  private final Map<AccountGroup.UUID, AccountGroup> out;
 
   @Inject
   GroupInfoCacheFactory(final GroupCache groupCache) {
     this.groupCache = groupCache;
-    this.out = new HashMap<AccountGroup.Id, AccountGroup>();
+    this.out = new HashMap<AccountGroup.UUID, AccountGroup>();
   }
 
   /**
    * Indicate a group will be needed later on.
    *
-   * @param id identity that will be needed in the future; may be null.
+   * @param uuid identity that will be needed in the future; may be null.
    */
-  public void want(final AccountGroup.Id id) {
-    if (id != null && !out.containsKey(id)) {
-      out.put(id, groupCache.get(id));
+  public void want(final AccountGroup.UUID uuid) {
+    if (uuid != null && !out.containsKey(uuid)) {
+      out.put(uuid, groupCache.get(uuid));
     }
   }
 
   /** Indicate one or more groups will be needed later on. */
-  public void want(final Iterable<AccountGroup.Id> ids) {
-    for (final AccountGroup.Id id : ids) {
-      want(id);
+  public void want(final Iterable<AccountGroup.UUID> uuids) {
+    for (final AccountGroup.UUID uuid : uuids) {
+      want(uuid);
     }
   }
 
-  public AccountGroup get(final AccountGroup.Id id) {
-    want(id);
-    return out.get(id);
+  public AccountGroup get(final AccountGroup.UUID uuid) {
+    want(uuid);
+    return out.get(uuid);
   }
 
   /**
@@ -68,6 +68,7 @@
   public GroupInfoCache create() {
     final List<GroupInfo> r = new ArrayList<GroupInfo>(out.size());
     for (final AccountGroup a : out.values()) {
+      if (a == null) continue;
       r.add(new GroupInfo(a));
     }
     return new GroupInfoCache(r);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
index 84282b5..16fcc0b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
@@ -18,7 +18,7 @@
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
@@ -111,9 +111,9 @@
       }
     }
     if (groupDetail.includes != null) {
-      for (final AccountGroupInclude groupInclude : groupDetail.includes) {
+      for (final AccountGroupIncludeByUuid groupInclude : groupDetail.includes) {
         final AccountGroup includedGroup =
-            groupCache.get(groupInclude.getIncludeId());
+            groupCache.get(groupInclude.getIncludeUUID());
         if (!seen.contains(includedGroup.getGroupUUID())) {
           members.addAll(listAccounts(includedGroup.getGroupUUID(), project, seen));
         }
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 1ff1a3e..0de2b044 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
@@ -18,8 +18,8 @@
 import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.client.AccountGroupName;
@@ -35,7 +35,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 
 public class PerformCreateGroup {
@@ -89,7 +88,7 @@
       final String groupDescription, final boolean visibleToAll,
       final AccountGroup.Id ownerGroupId,
       final Collection<? extends Account.Id> initialMembers,
-      final Collection<? extends AccountGroup.Id> initialGroups)
+      final Collection<? extends AccountGroup.UUID> initialGroups)
       throws OrmException, NameAlreadyUsedException, PermissionDeniedException {
     if (!currentUser.getCapabilities().canCreateGroup()) {
       throw new PermissionDeniedException(String.format(
@@ -160,26 +159,25 @@
   }
 
   private void addGroups(final AccountGroup.Id groupId,
-      final Collection<? extends AccountGroup.Id> groups) throws OrmException {
-    final List<AccountGroupInclude> includeList =
-      new ArrayList<AccountGroupInclude>();
-    final List<AccountGroupIncludeAudit> includesAudit =
-      new ArrayList<AccountGroupIncludeAudit>();
-    for (AccountGroup.Id includeId : groups) {
-      final AccountGroupInclude groupInclude =
-        new AccountGroupInclude(new AccountGroupInclude.Key(groupId, includeId));
+      final Collection<? extends AccountGroup.UUID> groups) throws OrmException {
+    final List<AccountGroupIncludeByUuid> includeList =
+      new ArrayList<AccountGroupIncludeByUuid>();
+    final List<AccountGroupIncludeByUuidAudit> includesAudit =
+      new ArrayList<AccountGroupIncludeByUuidAudit>();
+    for (AccountGroup.UUID includeUUID : groups) {
+      final AccountGroupIncludeByUuid groupInclude =
+        new AccountGroupIncludeByUuid(new AccountGroupIncludeByUuid.Key(groupId, includeUUID));
       includeList.add(groupInclude);
 
-      final AccountGroupIncludeAudit audit =
-        new AccountGroupIncludeAudit(groupInclude, currentUser.getAccountId());
+      final AccountGroupIncludeByUuidAudit audit =
+        new AccountGroupIncludeByUuidAudit(groupInclude, currentUser.getAccountId());
       includesAudit.add(audit);
     }
-    db.accountGroupIncludes().insert(includeList);
-    db.accountGroupIncludesAudit().insert(includesAudit);
+    db.accountGroupIncludesByUuid().insert(includeList);
+    db.accountGroupIncludesByUuidAudit().insert(includesAudit);
 
-    for (AccountGroup group : db.accountGroups().get(
-        new HashSet<AccountGroup.Id>(groups))) {
-      groupIncludeCache.evictInclude(group.getGroupUUID());
+    for (AccountGroup.UUID uuid : groups) {
+      groupIncludeCache.evictInclude(uuid);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java
index d3b2c83..54f0733 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
-import com.google.gerrit.common.data.GroupList;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -28,6 +28,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
@@ -43,6 +44,7 @@
 
   private boolean onlyVisibleToAll;
   private AccountGroup.Type groupType;
+  private String match;
 
   @Inject
   VisibleGroups(final Provider<IdentifiedUser> currentUser,
@@ -61,11 +63,15 @@
     this.groupType = groupType;
   }
 
-  public GroupList get() {
-    return createGroupList(filterGroups(groupCache.all()));
+  public void setMatch(final String match) {
+    this.match = match;
   }
 
-  public GroupList get(final Collection<ProjectControl> projects)
+  public List<AccountGroup> get() {
+    return filterGroups(groupCache.all());
+  }
+
+  public List<AccountGroup> get(final Collection<ProjectControl> projects)
       throws NoSuchGroupException {
     Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
     for (final ProjectControl projectControl : projects) {
@@ -78,7 +84,7 @@
         groups.put(group.getGroupUUID(), group);
       }
     }
-    return createGroupList(filterGroups(groups.values()));
+    return filterGroups(groups.values());
   }
 
   /**
@@ -87,7 +93,7 @@
    * groups.
    * @See GroupMembership#getKnownGroups()
    */
-  public GroupList get(final IdentifiedUser user) throws NoSuchGroupException {
+  public List<AccountGroup> get(final IdentifiedUser user) throws NoSuchGroupException {
     if (identifiedUser.get().getAccountId().equals(user.getAccountId())
         || identifiedUser.get().getCapabilities().canAdministrateServer()) {
       Set<AccountGroup.UUID> mine = user.getEffectiveGroups().getKnownGroups();
@@ -98,7 +104,7 @@
           groups.put(groupId, group);
         }
       }
-      return createGroupList(filterGroups(groups.values()));
+      return filterGroups(groups.values());
     } else {
       throw new NoSuchGroupException("Groups of user '" + user.getAccountId()
           + "' are not visible.");
@@ -110,6 +116,12 @@
     final boolean isAdmin =
         identifiedUser.get().getCapabilities().canAdministrateServer();
     for (final AccountGroup group : groups) {
+      if (!Strings.isNullOrEmpty(match)) {
+        if (!group.getName().toLowerCase(Locale.US)
+            .contains(match.toLowerCase(Locale.US))) {
+          continue;
+        }
+      }
       if (!isAdmin) {
         final GroupControl c = groupControlFactory.controlFor(group);
         if (!c.isVisible()) {
@@ -125,9 +137,4 @@
     Collections.sort(filteredGroups, new GroupComparator());
     return filteredGroups;
   }
-
-  private GroupList createGroupList(final List<AccountGroup> groups) {
-    return new GroupList(groups, identifiedUser.get()
-        .getCapabilities().canCreateGroup());
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
index efd4980..11a9edd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.change.PostReview.NotifyHandling;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.CommentSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -46,6 +47,7 @@
 
   interface Factory {
     EmailReviewComments create(
+        NotifyHandling notify,
         Change change,
         PatchSet patchSet,
         Account.Id authorId,
@@ -59,6 +61,7 @@
   private final SchemaFactory<ReviewDb> schemaFactory;
   private final ThreadLocalRequestContext requestContext;
 
+  private final PostReview.NotifyHandling notify;
   private final Change change;
   private final PatchSet patchSet;
   private final Account.Id authorId;
@@ -73,6 +76,7 @@
       CommentSender.Factory commentSenderFactory,
       SchemaFactory<ReviewDb> schemaFactory,
       ThreadLocalRequestContext requestContext,
+      @Assisted NotifyHandling notify,
       @Assisted Change change,
       @Assisted PatchSet patchSet,
       @Assisted Account.Id authorId,
@@ -83,6 +87,7 @@
     this.commentSenderFactory = commentSenderFactory;
     this.schemaFactory = schemaFactory;
     this.requestContext = requestContext;
+    this.notify = notify;
     this.change = change;
     this.patchSet = patchSet;
     this.authorId = authorId;
@@ -122,7 +127,7 @@
         }
       });
 
-      CommentSender cm = commentSenderFactory.create(change);
+      CommentSender cm = commentSenderFactory.create(notify, change);
       cm.setFrom(authorId);
       cm.setPatchSet(patchSet, patchSetInfoFactory.get(change, patchSet));
       cm.setChangeMessage(message);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
index d165a4a..1a9d1f2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -76,12 +76,19 @@
      * described in this input request.
      */
     public DraftHandling drafts = DraftHandling.DELETE;
+
+    /** Who to send email notifications to after review is stored. */
+    public NotifyHandling notify = NotifyHandling.ALL;
   }
 
   public static enum DraftHandling {
     DELETE, PUBLISH, KEEP;
   }
 
+  public static enum NotifyHandling {
+    NONE, OWNER, OWNER_REVIEWERS, ALL;
+  }
+
   static class Comment {
     String id;
     GetDraft.Side side;
@@ -132,6 +139,9 @@
     if (input.comments != null) {
       checkComments(input.comments);
     }
+    if (input.notify == null) {
+      input.notify = NotifyHandling.NONE;
+    }
 
     db.changes().beginTransaction(revision.getChange().getId());
     try {
@@ -151,8 +161,9 @@
       db.rollback();
     }
 
-    if (message != null) {
+    if (input.notify.compareTo(NotifyHandling.NONE) > 0 && message != null) {
       email.create(
+          input.notify,
           change,
           revision.getPatchSet(),
           revision.getAuthorId(),
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
index de7cfd4..3614b37 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -17,6 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
 import com.google.gerrit.extensions.restapi.ResourceConflictException;
 import com.google.gerrit.extensions.restapi.RestModifyView;
 import com.google.gerrit.reviewdb.client.Change;
@@ -26,25 +27,34 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.change.Revert.Input;
+import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.RevertedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.ssh.NoSshInfo;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import javax.annotation.Nullable;
 
 public class Revert implements RestModifyView<ChangeResource, Input> {
   private final ChangeHooks hooks;
   private final RevertedSender.Factory revertedSenderFactory;
+  private final CommitValidators.Factory commitValidatorsFactory;
   private final Provider<ReviewDb> dbProvider;
   private final ChangeJson json;
   private final GitRepositoryManager gitManager;
   private final PersonIdent myIdent;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final GitReferenceUpdated replication;
+  private final String canonicalWebUrl;
 
   public static class Input {
     public String message;
@@ -53,20 +63,24 @@
   @Inject
   Revert(ChangeHooks hooks,
       RevertedSender.Factory revertedSenderFactory,
+      final CommitValidators.Factory commitValidatorsFactory,
       Provider<ReviewDb> dbProvider,
       ChangeJson json,
       GitRepositoryManager gitManager,
       final PatchSetInfoFactory patchSetInfoFactory,
       final GitReferenceUpdated replication,
-      @GerritPersonIdent final PersonIdent myIdent) {
+      @GerritPersonIdent final PersonIdent myIdent,
+      @CanonicalWebUrl @Nullable final String canonicalWebUrl) {
     this.hooks = hooks;
     this.revertedSenderFactory = revertedSenderFactory;
+    this.commitValidatorsFactory = commitValidatorsFactory;
     this.dbProvider = dbProvider;
     this.json = json;
     this.gitManager = gitManager;
     this.myIdent = myIdent;
     this.replication = replication;
     this.patchSetInfoFactory = patchSetInfoFactory;
+    this.canonicalWebUrl = canonicalWebUrl;
   }
 
   @Override
@@ -75,8 +89,7 @@
   }
 
   @Override
-  public Object apply(ChangeResource req, Input input)
-      throws Exception {
+  public Object apply(ChangeResource req, Input input) throws Exception {
     ChangeControl control = req.getControl();
     Change change = req.getChange();
     if (!control.canAddPatchSet()) {
@@ -85,14 +98,25 @@
       throw new ResourceConflictException("change is " + status(change));
     }
 
-    Change.Id revertedChangeId = ChangeUtil.revert(
-        change.currentPatchSetId(),
-        (IdentifiedUser) control.getCurrentUser(),
-        Strings.emptyToNull(input.message),
-        dbProvider.get(),
-        revertedSenderFactory, hooks, gitManager,
-        patchSetInfoFactory, replication, myIdent);
-    return json.format(revertedChangeId);
+    final Repository git = gitManager.openRepository(control.getProject().getNameKey());
+    try {
+      CommitValidators commitValidators =
+          commitValidatorsFactory.create(control.getRefControl(), new NoSshInfo(), git);
+
+      Change.Id revertedChangeId =
+          ChangeUtil.revert(control.getRefControl(), change.currentPatchSetId(),
+              (IdentifiedUser) control.getCurrentUser(),
+              commitValidators,
+              Strings.emptyToNull(input.message), dbProvider.get(),
+              revertedSenderFactory, hooks, git, patchSetInfoFactory,
+              replication, myIdent, canonicalWebUrl);
+
+      return json.format(revertedChangeId);
+    } catch (InvalidChangeOperationException e) {
+      throw new BadRequestException(e.getMessage());
+    } finally {
+      git.close();
+    }
   }
 
   private static String status(Change change) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
index a71e12e..1cf0bd2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
@@ -15,12 +15,28 @@
 
 package com.google.gerrit.server.changedetail;
 
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromApprovals;
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
+
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.ReviewResult;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.mail.MailUtil.MailRecipients;
+import com.google.gerrit.server.mail.ReplacePatchSetSender;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.AtomicUpdate;
@@ -28,9 +44,22 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Callable;
 
 public class PublishDraft implements Callable<ReviewResult> {
+  private static final Logger log =
+      LoggerFactory.getLogger(PublishDraft.class);
 
   public interface Factory {
     PublishDraft create(PatchSet.Id patchSetId);
@@ -39,22 +68,41 @@
   private final ChangeControl.Factory changeControlFactory;
   private final ReviewDb db;
   private final ChangeHooks hooks;
+  private final GitRepositoryManager repoManager;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final ApprovalsUtil approvalsUtil;
+  private final AccountResolver accountResolver;
+  private final CreateChangeSender.Factory createChangeSenderFactory;
+  private final ReplacePatchSetSender.Factory replacePatchSetFactory;
 
   private final PatchSet.Id patchSetId;
 
   @Inject
-  PublishDraft(ChangeControl.Factory changeControlFactory,
-      ReviewDb db, @Assisted final PatchSet.Id patchSetId,
-      final ChangeHooks hooks) {
+  PublishDraft(final ChangeControl.Factory changeControlFactory,
+      final ReviewDb db, final ChangeHooks hooks,
+      final GitRepositoryManager repoManager,
+      final PatchSetInfoFactory patchSetInfoFactory,
+      final ApprovalsUtil approvalsUtil,
+      final AccountResolver accountResolver,
+      final CreateChangeSender.Factory createChangeSenderFactory,
+      final ReplacePatchSetSender.Factory replacePatchSetFactory,
+      @Assisted final PatchSet.Id patchSetId) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.hooks = hooks;
+    this.repoManager = repoManager;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.approvalsUtil = approvalsUtil;
+    this.accountResolver = accountResolver;
+    this.createChangeSenderFactory = createChangeSenderFactory;
+    this.replacePatchSetFactory = replacePatchSetFactory;
 
     this.patchSetId = patchSetId;
   }
 
   @Override
-  public ReviewResult call() throws NoSuchChangeException, OrmException {
+  public ReviewResult call() throws NoSuchChangeException, OrmException,
+      IOException, PatchSetInfoNotAvailableException {
     final ReviewResult result = new ReviewResult();
 
     final Change.Id changeId = patchSetId.getParentKey();
@@ -74,7 +122,7 @@
       result.addError(new ReviewResult.Error(
           ReviewResult.Error.Type.PUBLISH_NOT_PERMITTED));
     } else {
-      final PatchSet updatedPatch = db.patchSets().atomicUpdate(patchSetId,
+      final PatchSet updatedPatchSet = db.patchSets().atomicUpdate(patchSetId,
           new AtomicUpdate<PatchSet>() {
         @Override
         public PatchSet update(PatchSet patchset) {
@@ -95,11 +143,76 @@
         }
       });
 
-      if (!updatedPatch.isDraft() || updatedChange.getStatus() == Change.Status.NEW) {
-        hooks.doDraftPublishedHook(updatedChange, updatedPatch, db);
+      if (!updatedPatchSet.isDraft() || updatedChange.getStatus() == Change.Status.NEW) {
+        hooks.doDraftPublishedHook(updatedChange, updatedPatchSet, db);
+
+        sendNotifications(control.getChange().getStatus() == Change.Status.DRAFT,
+            (IdentifiedUser) control.getCurrentUser(), updatedChange, updatedPatchSet);
       }
     }
 
     return result;
   }
+
+  private void sendNotifications(final boolean newChange,
+      final IdentifiedUser currentUser, final Change updatedChange,
+      final PatchSet updatedPatchSet) throws OrmException, IOException,
+      PatchSetInfoNotAvailableException {
+    final Repository git = repoManager.openRepository(updatedChange.getProject());
+    try {
+      final RevWalk revWalk = new RevWalk(git);
+      final RevCommit commit;
+      try {
+        commit = revWalk.parseCommit(ObjectId.fromString(updatedPatchSet.getRevision().get()));
+      } finally {
+        revWalk.release();
+      }
+      final PatchSetInfo info = patchSetInfoFactory.get(commit, updatedPatchSet.getId());
+      final List<FooterLine> footerLines = commit.getFooterLines();
+      final Account.Id me = currentUser.getAccountId();
+      final MailRecipients recipients =
+          getRecipientsFromFooters(accountResolver, updatedPatchSet, footerLines);
+      recipients.remove(me);
+
+      if (newChange) {
+        approvalsUtil.addReviewers(db, updatedChange, updatedPatchSet, info,
+            recipients.getReviewers(), Collections.<Account.Id> emptySet());
+        try {
+          CreateChangeSender cm = createChangeSenderFactory.create(updatedChange);
+          cm.setFrom(me);
+          cm.setPatchSet(updatedPatchSet, info);
+          cm.addReviewers(recipients.getReviewers());
+          cm.addExtraCC(recipients.getCcOnly());
+          cm.send();
+        } catch (Exception e) {
+          log.error("Cannot send email for new change " + updatedChange.getId(), e);
+        }
+      } else {
+        final List<PatchSetApproval> patchSetApprovals =
+            db.patchSetApprovals().byChange(updatedChange.getId()).toList();
+        final MailRecipients oldRecipients =
+            getRecipientsFromApprovals(patchSetApprovals);
+        approvalsUtil.addReviewers(db, updatedChange, updatedPatchSet, info,
+            recipients.getReviewers(), oldRecipients.getAll());
+        final ChangeMessage msg =
+            new ChangeMessage(new ChangeMessage.Key(updatedChange.getId(),
+                ChangeUtil.messageUUID(db)), me,
+                updatedPatchSet.getCreatedOn(), updatedPatchSet.getId());
+        msg.setMessage("Uploaded patch set " + updatedPatchSet.getPatchSetId() + ".");
+        try {
+          ReplacePatchSetSender cm = replacePatchSetFactory.create(updatedChange);
+          cm.setFrom(me);
+          cm.setPatchSet(updatedPatchSet, info);
+          cm.setChangeMessage(msg);
+          cm.addReviewers(recipients.getReviewers());
+          cm.addExtraCC(recipients.getCcOnly());
+          cm.send();
+        } catch (Exception e) {
+          log.error("Cannot send email for new patch set " + updatedPatchSet.getId(), e);
+        }
+      }
+    } finally {
+      git.close();
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
index 568966c..0c1bef3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
@@ -95,6 +95,8 @@
   /**
    * Rebases the change of the given patch set.
    *
+   * It is verified that the current user is allowed to do the rebase.
+   *
    * If the patch set has no dependency to an open change, then the change is
    * rebased on the tip of the destination branch.
    *
@@ -122,6 +124,11 @@
     final Change.Id changeId = patchSetId.getParentKey();
     final ChangeControl changeControl =
         changeControlFactory.validateFor(changeId);
+    if (!changeControl.canRebase()) {
+      throw new InvalidChangeOperationException(
+          "Cannot rebase: New patch sets are not allowed to be added to change: "
+              + changeId.toString());
+    }
     final Change change = changeControl.getChange();
     Repository git = null;
     RevWalk rw = null;
@@ -297,14 +304,6 @@
       OrmException, IOException, InvalidChangeOperationException,
       PathConflictException {
     Change change = chg;
-    final ChangeControl changeControl =
-        changeControlFactory.validateFor(change);
-    if (!changeControl.canRebase()) {
-      throw new InvalidChangeOperationException(
-          "Cannot rebase: New patch sets are not allowed to be added to change: "
-              + change.getId().toString());
-    }
-
     final PatchSet originalPatchSet = db.patchSets().get(patchSetId);
 
     final RevCommit rebasedCommit;
@@ -380,7 +379,7 @@
           new ChangeMessage(new ChangeMessage.Key(change.getId(),
               ChangeUtil.messageUUID(db)), uploader, patchSetId);
       cmsg.setMessage("Patch Set " + change.currentPatchSetId().get()
-          + ": Patch Set " + patchSetId.get() + " was rebased onto the latest head");
+          + ": Patch Set " + patchSetId.get() + " was rebased");
       db.changeMessages().insert(Collections.singleton(cmsg));
       db.commit();
     } finally {
@@ -421,7 +420,7 @@
 
     if (merger.getResultTreeId() == null) {
       throw new PathConflictException(
-          "The rebase failed since conflicts occured during the merge.");
+          "The change could not be rebased due to a path conflict during merge.");
     }
 
     final CommitBuilder cb = new CommitBuilder();
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 cc754de..ff8a5eb 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
@@ -49,6 +49,7 @@
 import com.google.gerrit.server.account.InternalGroupBackend;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.account.UniversalGroupBackend;
+import com.google.gerrit.server.account.VisibleGroups;
 import com.google.gerrit.server.auth.AuthBackend;
 import com.google.gerrit.server.auth.InternalAuthBackend;
 import com.google.gerrit.server.auth.UniversalAuthBackend;
@@ -65,6 +66,7 @@
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.EmailModule;
 import com.google.gerrit.server.mail.FromAddressGenerator;
 import com.google.gerrit.server.mail.FromAddressGeneratorProvider;
@@ -151,6 +153,7 @@
     factory(CapabilityControl.Factory.class);
     factory(ChangeQueryBuilder.Factory.class);
     factory(GroupInfoCacheFactory.Factory.class);
+    factory(VisibleGroups.Factory.class);
     factory(InternalUser.Factory.class);
     factory(ProjectNode.Factory.class);
     factory(ProjectState.Factory.class);
@@ -194,6 +197,7 @@
     install(new AuditModule());
     install(new com.google.gerrit.server.account.Module());
     install(new com.google.gerrit.server.change.Module());
+    install(new com.google.gerrit.server.group.Module());
     install(new com.google.gerrit.server.project.Module());
 
     bind(GitReferenceUpdated.class);
@@ -203,6 +207,7 @@
     DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
     DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ChangeCache.class);
     DynamicSet.setOf(binder(), CommitValidationListener.class);
+    factory(CommitValidators.Factory.class);
 
     bind(AnonymousUser.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 9d72bf6..2784f30 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,7 +24,6 @@
 import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.account.PerformCreateGroup;
 import com.google.gerrit.server.account.PerformRenameGroup;
-import com.google.gerrit.server.account.VisibleGroups;
 import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
 import com.google.gerrit.server.changedetail.PublishDraft;
 import com.google.gerrit.server.git.AsyncReceiveCommits;
@@ -85,7 +84,6 @@
     factory(MergeFailSender.Factory.class);
     factory(PerformCreateGroup.Factory.class);
     factory(PerformRenameGroup.Factory.class);
-    factory(VisibleGroups.Factory.class);
     factory(GroupDetailFactory.Factory.class);
     factory(GroupMembers.Factory.class);
     factory(CreateProject.Factory.class);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java
index d0c4742..4ef75c3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java
@@ -23,6 +23,7 @@
 import static com.google.gerrit.server.git.MergeUtil.mergeOneCommit;
 import static com.google.gerrit.server.git.MergeUtil.getApprovalsForCommit;
 
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -185,10 +186,11 @@
     n.change.setCurrentPatchSet(patchSetInfoFactory.get(newCommit, ps.getId()));
     args.db.changes().update(Collections.singletonList(n.change));
 
+    final List<PatchSetApproval> approvals = Lists.newArrayList();
     for (PatchSetApproval a : getApprovalsForCommit(args.db, n)) {
-      args.db.patchSetApprovals().insert(
-          Collections.singleton(new PatchSetApproval(ps.getId(), a)));
+      approvals.add(new PatchSetApproval(ps.getId(), a));
     }
+    args.db.patchSetApprovals().insert(approvals);
 
     final RefUpdate ru = args.repo.updateRef(ps.getRefName());
     ru.setExpectedOldObjectId(ObjectId.zeroId());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
index 7dbba04..5a39aae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
@@ -28,7 +28,7 @@
   ALREADY_MERGED(""),
 
   /** */
-  PATH_CONFLICT("Your change could not be merged due to a path conflict.\n"
+  PATH_CONFLICT("The change could not be merged due to a path conflict.\n"
                   + "\n"
                   + "Please rebase the change locally and upload the rebased commit for review."),
 
@@ -45,7 +45,7 @@
   NO_SUBMIT_TYPE(""),
 
   /** */
-  CRISS_CROSS_MERGE("Your change requires a recursive merge to resolve.\n"
+  CRISS_CROSS_MERGE("The change requires a recursive merge to resolve.\n"
                   + "\n"
                   + "Please merge (or rebase) the change locally and upload the resolution for review."),
 
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 44cb9a7..e23e849 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
@@ -177,10 +177,7 @@
       // It doesn't exist under any of the standard permutations
       // of the repository name, so prefer the standard bare name.
       //
-      String n = name.get();
-      if (!n.endsWith(Constants.DOT_GIT_EXT)) {
-        n = n + Constants.DOT_GIT_EXT;
-      }
+      String n = name.get() + Constants.DOT_GIT_EXT;
       loc = FileKey.exact(new File(basePath, n), FS.DETECTED);
     }
 
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 23593e5..9200db4 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
@@ -16,6 +16,8 @@
 
 import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
 import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromApprovals;
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
 import static org.eclipse.jgit.lib.Constants.R_HEADS;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
@@ -37,11 +39,8 @@
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.gerrit.common.ChangeHookRunner.HookResult;
 import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.errors.NoSuchAccountException;
-import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -65,9 +64,10 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
 import com.google.gerrit.server.git.validators.CommitValidationException;
-import com.google.gerrit.server.git.validators.CommitValidationListener;
 import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -86,8 +86,6 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import com.jcraft.jsch.HostKey;
-
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
@@ -113,13 +111,10 @@
 import org.eclipse.jgit.transport.ReceiveCommand.Result;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.UploadPack;
-import org.eclipse.jgit.util.SystemReader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URL;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -143,8 +138,6 @@
   private static final Pattern NEW_PATCHSET =
       Pattern.compile("^refs/changes/(?:[0-9][0-9]/)?([1-9][0-9]*)(?:/new)?$");
 
-  private static final FooterKey REVIEWED_BY = new FooterKey("Reviewed-by");
-  private static final FooterKey TESTED_BY = new FooterKey("Tested-by");
   private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
 
   private static final String COMMAND_REJECTION_MESSAGE_FOOTER =
@@ -251,7 +244,7 @@
   private final GitRepositoryManager repoManager;
   private final ProjectCache projectCache;
   private final String canonicalWebUrl;
-  private final PersonIdent gerritIdent;
+  private final CommitValidators.Factory commitValidatorsFactory;
   private final TrackingFooters trackingFooters;
   private final TagCache tagCache;
   private final WorkQueue workQueue;
@@ -290,7 +283,6 @@
   private Task commandProgress;
   private MessageSender messageSender;
   private BatchRefUpdate batch;
-  private final DynamicSet<CommitValidationListener> commitValidators;
 
   @Inject
   ReceiveCommits(final ReviewDb db,
@@ -307,6 +299,7 @@
       final GitRepositoryManager repoManager,
       final TagCache tagCache,
       final ChangeCache changeCache,
+      final CommitValidators.Factory commitValidatorsFactory,
       @CanonicalWebUrl @Nullable final String canonicalWebUrl,
       @GerritPersonIdent final PersonIdent gerritIdent,
       final TrackingFooters trackingFooters,
@@ -314,7 +307,6 @@
       @ChangeUpdateExecutor ListeningExecutorService changeUpdateExector,
       final RequestScopePropagator requestScopePropagator,
       final SshInfo sshInfo,
-      final DynamicSet<CommitValidationListener> commitValidationListeners,
       final AllProjectsName allProjectsName,
       @Assisted final ProjectControl projectControl,
       @Assisted final Repository repo,
@@ -333,9 +325,9 @@
     this.projectCache = projectCache;
     this.repoManager = repoManager;
     this.canonicalWebUrl = canonicalWebUrl;
-    this.gerritIdent = gerritIdent;
     this.trackingFooters = trackingFooters;
     this.tagCache = tagCache;
+    this.commitValidatorsFactory = commitValidatorsFactory;
     this.workQueue = workQueue;
     this.changeUpdateExector = changeUpdateExector;
     this.requestScopePropagator = requestScopePropagator;
@@ -349,7 +341,6 @@
     this.rejectCommits = loadRejectCommitsMap();
 
     this.subOpFactory = subOpFactory;
-    this.commitValidators = commitValidationListeners;
 
     this.messageSender = new ReceivePackMessageSender();
 
@@ -688,7 +679,7 @@
       reject(newChange, "internal server error");
       log.error(String.format(
           "Only %d of %d new change refs created in %s; aborting",
-          okToInsert, newChanges.size(), project.getName()));
+          okToInsert, replaceCount + newChanges.size(), project.getName()));
       return;
     }
 
@@ -741,16 +732,6 @@
     return displayName;
   }
 
-  private Account.Id toAccountId(final String nameOrEmail) throws OrmException,
-      NoSuchAccountException {
-    final Account a = accountResolver.findByNameOrEmail(nameOrEmail);
-    if (a == null) {
-      throw new NoSuchAccountException("\"" + nameOrEmail
-          + "\" is not registered");
-    }
-    return a.getId();
-  }
-
   private void parseCommands(final Collection<ReceiveCommand> commands) {
     for (final ReceiveCommand cmd : commands) {
       if (cmd.getResult() != NOT_ATTEMPTED) {
@@ -1203,6 +1184,7 @@
           //
           continue;
         }
+
         if (!validCommit(destBranchCtl, newChange, c)) {
           // Not a change the user can propose? Abort as early as possible.
           //
@@ -1385,26 +1367,10 @@
 
     private void insertChange(ReviewDb db) throws OrmException {
       final Account.Id me = currentUser.getAccountId();
-      final Set<Account.Id> reviewers = new HashSet<Account.Id>(reviewerId);
-      final Set<Account.Id> cc = new HashSet<Account.Id>(ccId);
       final List<FooterLine> footerLines = commit.getFooterLines();
-      for (final FooterLine footerLine : footerLines) {
-        try {
-          if (ps.isDraft()) {
-            continue;
-          }
-          if (isReviewer(footerLine)) {
-            reviewers.add(toAccountId(footerLine.getValue().trim()));
-          } else if (footerLine.matches(FooterKey.CC)) {
-            cc.add(toAccountId(footerLine.getValue().trim()));
-          }
-        } catch (NoSuchAccountException e) {
-          continue;
-        }
-      }
-      reviewers.remove(me);
-      cc.remove(me);
-      cc.removeAll(reviewers);
+      final MailRecipients recipients = new MailRecipients(reviewerId, ccId);
+      recipients.add(getRecipientsFromFooters(accountResolver, ps, footerLines));
+      recipients.remove(me);
 
       db.changes().beginTransaction(change.getId());
       try {
@@ -1413,7 +1379,7 @@
         db.changes().insert(Collections.singleton(change));
         ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
         approvalsUtil.addReviewers(db, change, ps, info,
-            reviewers, Collections.<Account.Id> emptySet());
+            recipients.getReviewers(), Collections.<Account.Id> emptySet());
         db.commit();
       } finally {
         db.rollback();
@@ -1431,8 +1397,8 @@
                 createChangeSenderFactory.create(change);
             cm.setFrom(me);
             cm.setPatchSet(ps, info);
-            cm.addReviewers(reviewers);
-            cm.addExtraCC(cc);
+            cm.addReviewers(recipients.getReviewers());
+            cm.addExtraCC(recipients.getCcOnly());
             cm.send();
           } catch (Exception e) {
             log.error("Cannot send email for new change " + change.getId(), e);
@@ -1447,13 +1413,6 @@
     }
   }
 
-  private static boolean isReviewer(final FooterLine candidateFooterLine) {
-    return candidateFooterLine.matches(FooterKey.SIGNED_OFF_BY)
-        || candidateFooterLine.matches(FooterKey.ACKED_BY)
-        || candidateFooterLine.matches(REVIEWED_BY)
-        || candidateFooterLine.matches(TESTED_BY);
-  }
-
   private void preparePatchSetsForReplace() {
     try {
       readChangesForReplace();
@@ -1568,6 +1527,7 @@
       }
 
       rp.getRevWalk().parseBody(newCommit);
+
       if (!validCommit(changeCtl.getRefControl(), inputCommand, newCommit)) {
         return false;
       }
@@ -1699,26 +1659,10 @@
 
     PatchSet.Id insertPatchSet(ReviewDb db) throws OrmException {
       final Account.Id me = currentUser.getAccountId();
-      final Set<Account.Id> reviewers = new HashSet<Account.Id>(reviewerId);
-      final Set<Account.Id> cc = new HashSet<Account.Id>(ccId);
       final List<FooterLine> footerLines = newCommit.getFooterLines();
-      for (final FooterLine footerLine : footerLines) {
-        try {
-          if (isReviewer(footerLine)) {
-            reviewers.add(toAccountId(footerLine.getValue().trim()));
-          } else if (footerLine.matches(FooterKey.CC)) {
-            cc.add(toAccountId(footerLine.getValue().trim()));
-          }
-        } catch (NoSuchAccountException e) {
-          continue;
-        }
-      }
-      reviewers.remove(me);
-      cc.remove(me);
-      cc.removeAll(reviewers);
-
-      final Set<Account.Id> oldReviewers = new HashSet<Account.Id>();
-      final Set<Account.Id> oldCC = new HashSet<Account.Id>();
+      final MailRecipients recipients = new MailRecipients(reviewerId, ccId);
+      recipients.add(getRecipientsFromFooters(accountResolver, newPatchSet, footerLines));
+      recipients.remove(me);
 
       db.changes().beginTransaction(change.getId());
       try {
@@ -1738,22 +1682,11 @@
 
         List<PatchSetApproval> patchSetApprovals =
             approvalsUtil.copyVetosToPatchSet(db, newPatchSet.getId());
-
-        final Set<Account.Id> haveApprovals = new HashSet<Account.Id>();
-        oldReviewers.clear();
-        oldCC.clear();
-
-        for (PatchSetApproval a : patchSetApprovals) {
-          haveApprovals.add(a.getAccountId());
-          if (a.getValue() != 0) {
-            oldReviewers.add(a.getAccountId());
-          } else {
-            oldCC.add(a.getAccountId());
-          }
-        }
-
+        final MailRecipients oldRecipients =
+            getRecipientsFromApprovals(patchSetApprovals);
         approvalsUtil.addReviewers(db, change, newPatchSet, info,
-            reviewers, haveApprovals);
+            recipients.getReviewers(), oldRecipients.getAll());
+        recipients.add(oldRecipients);
 
         msg =
             new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
@@ -1812,6 +1745,9 @@
         markChangeMergedByPush(db, this);
       }
 
+      if (cmd.getResult() == NOT_ATTEMPTED) {
+        cmd.execute(rp);
+      }
       replication.fire(project.getNameKey(), newPatchSet.getRefName());
       hooks.doPatchsetCreatedHook(change, newPatchSet, db);
       if (mergedIntoRef != null) {
@@ -1828,10 +1764,8 @@
             cm.setFrom(me);
             cm.setPatchSet(newPatchSet, info);
             cm.setChangeMessage(msg);
-            cm.addReviewers(reviewers);
-            cm.addExtraCC(cc);
-            cm.addReviewers(oldReviewers);
-            cm.addExtraCC(oldCC);
+            cm.addReviewers(recipients.getReviewers());
+            cm.addExtraCC(recipients.getCcOnly());
             cm.send();
           } catch (Exception e) {
             log.error("Cannot send email for new patch set " + newPatchSet.getId(), e);
@@ -1950,279 +1884,22 @@
 
   private boolean validCommit(final RefControl ctl, final ReceiveCommand cmd,
       final RevCommit c) throws MissingObjectException, IOException {
-    rp.getRevWalk().parseBody(c);
-    final PersonIdent committer = c.getCommitterIdent();
-    final PersonIdent author = c.getAuthorIdent();
 
-    // Require permission to upload merges.
-    if (c.getParentCount() > 1 && !ctl.canUploadMerges()) {
-      reject(cmd, "you are not allowed to upload merges");
+    CommitReceivedEvent receiveEvent =
+        new CommitReceivedEvent(cmd, project, ctl.getRefName(), c, currentUser);
+    CommitValidators commitValidators =
+        commitValidatorsFactory.create(ctl, sshInfo, repo);
+
+    try {
+      messages.addAll(commitValidators.validateForReceiveCommits(receiveEvent));
+    } catch (CommitValidationException e) {
+      messages.addAll(e.getMessages());
+      reject(cmd, e.getMessage());
       return false;
     }
-
-    // Don't allow the user to amend a merge created by Gerrit Code Review.
-    // This seems to happen all too often, due to users not paying any
-    // attention to what they are doing.
-    //
-    if (c.getParentCount() > 1
-        && author.getName().equals(gerritIdent.getName())
-        && author.getEmailAddress().equals(gerritIdent.getEmailAddress())
-        && !ctl.canForgeGerritServerIdentity()) {
-      reject(cmd, "do not amend merges not made by you");
-      return false;
-    }
-
-    // Require that author matches the uploader.
-    //
-    if (!currentUser.getEmailAddresses().contains(author.getEmailAddress())
-        && !ctl.canForgeAuthor()) {
-      sendInvalidEmailError(c, "author", author);
-      reject(cmd, "invalid author");
-      return false;
-    }
-
-    // Require that committer matches the uploader.
-    //
-    if (!currentUser.getEmailAddresses().contains(committer.getEmailAddress())
-        && !ctl.canForgeCommitter()) {
-      sendInvalidEmailError(c, "committer", committer);
-      reject(cmd, "invalid committer");
-      return false;
-    }
-
-    if (projectControl.getProjectState().isUseSignedOffBy()) {
-      // If the project wants Signed-off-by / Acked-by lines, verify we
-      // have them for the blamable parties involved on this change.
-      //
-      boolean sboAuthor = false, sboCommitter = false, sboMe = false;
-      for (final FooterLine footer : c.getFooterLines()) {
-        if (footer.matches(FooterKey.SIGNED_OFF_BY)) {
-          final String e = footer.getEmailAddress();
-          if (e != null) {
-            sboAuthor |= author.getEmailAddress().equals(e);
-            sboCommitter |= committer.getEmailAddress().equals(e);
-            sboMe |= currentUser.getEmailAddresses().contains(e);
-          }
-        }
-      }
-      if (!sboAuthor && !sboCommitter && !sboMe && !ctl.canForgeCommitter()) {
-        reject(cmd, "not Signed-off-by author/committer/uploader in commit message footer");
-        return false;
-      }
-    }
-
-    final List<String> idList = c.getFooterLines(CHANGE_ID);
-    if (MagicBranch.isMagicBranch(cmd.getRefName()) || NEW_PATCHSET.matcher(cmd.getRefName()).matches()) {
-      if (idList.isEmpty()) {
-        if (projectControl.getProjectState().isRequireChangeID()) {
-          String errMsg = "missing Change-Id in commit message footer";
-          reject(cmd, errMsg);
-          addMessage(getFixedCommitMsgWithChangeId(errMsg, c));
-          return false;
-        }
-      } else if (idList.size() > 1) {
-        reject(cmd, "multiple Change-Id lines in commit message footer");
-        return false;
-      } else {
-        final String v = idList.get(idList.size() - 1).trim();
-        if (!v.matches("^I[0-9a-f]{8,}.*$")) {
-          final String errMsg =
-              "missing or invalid Change-Id line format in commit message footer";
-          reject(cmd, errMsg);
-          addMessage(getFixedCommitMsgWithChangeId(errMsg, c));
-          return false;
-        }
-      }
-    }
-
-    // Check for banned commits to prevent them from entering the tree again.
-    if (rejectCommits.contains(c)) {
-      reject(cmd, "contains banned commit " + c.getName());
-      return false;
-    }
-
-    // If this is the special project configuration branch, validate the config.
-    if (GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())) {
-      try {
-        ProjectConfig cfg = new ProjectConfig(project.getNameKey());
-        cfg.load(repo, cmd.getNewId());
-        if (!cfg.getValidationErrors().isEmpty()) {
-          addError("Invalid project configuration:");
-          for (ValidationError err : cfg.getValidationErrors()) {
-            addError("  " + err.getMessage());
-          }
-          reject(cmd, "invalid project configuration");
-          log.error("User " + currentUser.getUserName()
-              + " tried to push invalid project configuration "
-              + cmd.getNewId().name() + " for " + project.getName());
-          return false;
-        }
-      } 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);
-        return false;
-      }
-    }
-
-    // Execute commit validation plugins
-    for (CommitValidationListener validator : commitValidators) {
-      try {
-        messages.addAll(validator.onCommitReceived(new CommitReceivedEvent(
-            cmd, project, ctl.getRefName(), c, currentUser)));
-      } catch (CommitValidationException error) {
-        messages.addAll(error.getMessages());
-        reject(cmd, error.getMessage());
-        return false;
-      }
-    }
-
     return true;
   }
 
-  /**
-   * Get the Gerrit hostname.
-   * @return the hostname from the canonical URL if it is configured,
-   * otherwise whatever the OS says the hostname is.
-   */
-  private String getGerritHost() {
-    String host;
-    if (canonicalWebUrl != null) {
-      try {
-        host = new URL(canonicalWebUrl).getHost();
-      } catch (MalformedURLException e) {
-        host = SystemReader.getInstance().getHostname();
-      }
-    } else {
-      host = SystemReader.getInstance().getHostname();
-    }
-    return host;
-  }
-
-  /**
-   * Get the Gerrit URL.
-   * @return the canonical URL (with any trailing slash removed) if it is
-   * configured, otherwise fall back to "http://hostname" where hostname is
-   * the value returned by {@link #getGerritHost()}.
-   */
-  private String getGerritUrl() {
-    if (canonicalWebUrl != null) {
-      if (canonicalWebUrl.endsWith("/")) {
-        return canonicalWebUrl.substring(0, canonicalWebUrl.lastIndexOf("/"));
-      }
-      return canonicalWebUrl;
-    } else {
-      return "http://" + getGerritHost();
-    }
-  }
-
-  /**
-   * Get the text with instructions for installing the commit-msg hook, specific
-   * to the server hostname and transport protocol.
-   * @return commit-msg hook installation instructions as a String.
-   */
-  private String getCommitMessageHookInstallationHint() {
-    final List<HostKey> hostKeys = sshInfo.getHostKeys();
-
-    // If there are no SSH keys, the commit-msg hook must be installed via HTTP(S)
-    if (hostKeys.isEmpty()) {
-      String p = ".git/hooks/commit-msg";
-      return String.format(
-          "  curl -o %s %s/tools/hooks/commit-msg ; chmod +x %s",
-          p, getGerritUrl(), p);
-    }
-
-    // SSH keys exist, so the hook can be installed with scp.
-    String sshHost;
-    int sshPort;
-    String host = hostKeys.get(0).getHost();
-    int c = host.lastIndexOf(':');
-    if (0 <= c) {
-      if (host.startsWith("*:")) {
-        sshHost = getGerritHost();
-      } else {
-        sshHost = host.substring(0, c);
-      }
-      sshPort = Integer.parseInt(host.substring(c+1));
-    } else {
-      sshHost = host;
-      sshPort = 22;
-    }
-
-    return String.format(
-        "  scp -p -P %d %s@%s:hooks/commit-msg .git/hooks/",
-        sshPort, currentUser.getUserName(), sshHost);
-  }
-
-  private String getFixedCommitMsgWithChangeId(String errMsg, RevCommit c) {
-    // We handle 3 cases:
-    // 1. No change id in the commit message at all.
-    // 2. change id last in the commit message but missing empty line to create the footer.
-    // 3. there is a change-id somewhere in the commit message, but we ignore it.
-    final String changeId = "Change-Id:";
-    StringBuilder sb = new StringBuilder();
-    sb.append("ERROR: ").append(errMsg);
-    sb.append('\n');
-    sb.append("Suggestion for commit message:\n");
-
-    if (c.getFullMessage().indexOf(changeId)==-1) {
-      sb.append(c.getFullMessage());
-      sb.append('\n');
-      sb.append(changeId).append(" I").append(c.name());
-    } else {
-      String lines[] = c.getFullMessage().trim().split("\n");
-      String lastLine = lines.length > 0 ? lines[lines.length - 1] : "";
-
-      if (lastLine.indexOf(changeId)==0) {
-        for (int i = 0; i < lines.length - 1; i++) {
-          sb.append(lines[i]);
-          sb.append('\n');
-        }
-
-        sb.append('\n');
-        sb.append(lastLine);
-      } else {
-        sb.append(c.getFullMessage());
-        sb.append('\n');
-        sb.append(changeId).append(" I").append(c.name());
-        sb.append('\n');
-        sb.append("Hint: A potential Change-Id was found, but it was not in the footer of the commit message.");
-      }
-    }
-    sb.append('\n');
-    sb.append('\n');
-    sb.append("Hint: To automatically insert Change-Id, install the hook:\n");
-    sb.append(getCommitMessageHookInstallationHint()).append('\n');
-    sb.append('\n');
-
-    return sb.toString();
-  }
-
-  private void sendInvalidEmailError(RevCommit c, String type, PersonIdent who) {
-    StringBuilder sb = new StringBuilder();
-    sb.append("\n");
-    sb.append("ERROR:  In commit " + c.name() + "\n");
-    sb.append("ERROR:  " + type + " email address " + who.getEmailAddress() + "\n");
-    sb.append("ERROR:  does not match your user account.\n");
-    sb.append("ERROR:\n");
-    if (currentUser.getEmailAddresses().isEmpty()) {
-      sb.append("ERROR:  You have not registered any email addresses.\n");
-    } else {
-      sb.append("ERROR:  The following addresses are currently registered:\n");
-      for (String address : currentUser.getEmailAddresses()) {
-        sb.append("ERROR:    " + address + "\n");
-      }
-    }
-    sb.append("ERROR:\n");
-    if (canonicalWebUrl != null) {
-      sb.append("ERROR:  To register an email address, please visit:\n");
-      sb.append("ERROR:  " + canonicalWebUrl + "#" + PageLinks.SETTINGS_CONTACT + "\n");
-    }
-    sb.append("\n");
-    addMessage(sb.toString());
-  }
-
   private void warnMalformedMessage(RevCommit c) {
     ObjectReader reader = rp.getRevWalk().getObjectReader();
     if (65 < c.getShortMessage().length()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
new file mode 100644
index 0000000..a53548b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -0,0 +1,576 @@
+// Copyright (C) 2012 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.validators;
+
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.ValidationError;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.util.MagicBranch;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import com.jcraft.jsch.HostKey;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterKey;
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.SystemReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+
+public class CommitValidators {
+  private static final Logger log = LoggerFactory
+      .getLogger(CommitValidators.class);
+
+  private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
+
+  private static final Pattern NEW_PATCHSET = Pattern
+      .compile("^refs/changes/(?:[0-9][0-9])?(/[1-9][0-9]*){1,2}(?:/new)?$");
+
+  public interface Factory {
+    CommitValidators create(RefControl refControl, SshInfo sshInfo,
+        Repository repo);
+  }
+
+  private final PersonIdent gerritIdent;
+  private final RefControl refControl;
+  private final String canonicalWebUrl;
+  private final SshInfo sshInfo;
+  private final Repository repo;
+  private final DynamicSet<CommitValidationListener> commitValidationListeners;
+
+  @Inject
+  CommitValidators(@GerritPersonIdent final PersonIdent gerritIdent,
+      @CanonicalWebUrl @Nullable final String canonicalWebUrl,
+      final DynamicSet<CommitValidationListener> commitValidationListeners,
+      @Assisted final SshInfo sshInfo,
+      @Assisted final Repository repo, @Assisted final RefControl refControl) {
+    this.gerritIdent = gerritIdent;
+    this.refControl = refControl;
+    this.canonicalWebUrl = canonicalWebUrl;
+    this.sshInfo = sshInfo;
+    this.repo = repo;
+    this.commitValidationListeners = commitValidationListeners;
+  }
+
+  public List<CommitValidationMessage> validateForReceiveCommits(
+      CommitReceivedEvent receiveEvent) throws CommitValidationException {
+
+    List<CommitValidationListener> validators =
+        new LinkedList<CommitValidationListener>();
+
+    validators.add(new UploadMergesPermissionValidator(refControl));
+    validators.add(new AmendedGerritMergeCommitValidationListener(
+        refControl, gerritIdent));
+    validators.add(new AuthorUploaderValidator(refControl, canonicalWebUrl));
+    validators.add(new CommitterUploaderValidator(refControl, canonicalWebUrl));
+    validators.add(new SignedOffByValidator(refControl, canonicalWebUrl));
+    validators.add(new ChangeIdValidator(refControl, canonicalWebUrl, sshInfo));
+    validators.add(new ConfigValidator(refControl, repo));
+    validators.add(new PluginCommitValidationListener(commitValidationListeners));
+
+    List<CommitValidationMessage> messages =
+        new LinkedList<CommitValidationMessage>();
+
+    try {
+      for (CommitValidationListener commitValidator : validators) {
+        messages.addAll(commitValidator.onCommitReceived(receiveEvent));
+      }
+    } catch (CommitValidationException e) {
+      // Keep the old messages (and their order) in case of an exception
+      messages.addAll(e.getMessages());
+      throw new CommitValidationException(e.getMessage(), messages);
+    }
+    return messages;
+  }
+
+  public List<CommitValidationMessage> validateForRevertCommits(
+      CommitReceivedEvent receiveEvent) throws CommitValidationException {
+
+    List<CommitValidationListener> validators =
+        new LinkedList<CommitValidationListener>();
+
+    validators.add(new UploadMergesPermissionValidator(refControl));
+    validators.add(new AmendedGerritMergeCommitValidationListener(
+        refControl, gerritIdent));
+    validators.add(new AuthorUploaderValidator(refControl, canonicalWebUrl));
+    validators.add(new SignedOffByValidator(refControl, canonicalWebUrl));
+    validators.add(new ChangeIdValidator(refControl, canonicalWebUrl, sshInfo));
+    validators.add(new ConfigValidator(refControl, repo));
+    validators.add(new PluginCommitValidationListener(commitValidationListeners));
+
+    List<CommitValidationMessage> messages =
+        new LinkedList<CommitValidationMessage>();
+
+    try {
+      for (CommitValidationListener commitValidator : validators) {
+        messages.addAll(commitValidator.onCommitReceived(receiveEvent));
+      }
+    } catch (CommitValidationException e) {
+      // Keep the old messages (and their order) in case of an exception
+      messages.addAll(e.getMessages());
+      throw new CommitValidationException(e.getMessage(), messages);
+    }
+    return messages;
+  }
+
+  public static class ChangeIdValidator implements CommitValidationListener {
+    private final RefControl refControl;
+    private final String canonicalWebUrl;
+    private final SshInfo sshInfo;
+
+    public ChangeIdValidator(RefControl refControl, String canonicalWebUrl,
+        SshInfo sshInfo) {
+      this.refControl = refControl;
+      this.canonicalWebUrl = canonicalWebUrl;
+      this.sshInfo = sshInfo;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+
+      final ProjectControl projectControl = refControl.getProjectControl();
+      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      final List<String> idList = receiveEvent.commit.getFooterLines(CHANGE_ID);
+
+      if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName())
+          || NEW_PATCHSET.matcher(receiveEvent.command.getRefName()).matches()) {
+        List<CommitValidationMessage> messages =
+            new LinkedList<CommitValidationMessage>();
+
+        if (idList.isEmpty()) {
+          if (projectControl.getProjectState().isRequireChangeID()) {
+            String errMsg = "missing Change-Id in commit message footer";
+            messages.add(getFixedCommitMsgWithChangeId(errMsg, receiveEvent.commit,
+                currentUser, canonicalWebUrl, sshInfo));
+            throw new CommitValidationException(errMsg, messages);
+          }
+        } else if (idList.size() > 1) {
+          throw new CommitValidationException(
+              "multiple Change-Id lines in commit message footer", messages);
+        } else {
+          final String v = idList.get(idList.size() - 1).trim();
+          if (!v.matches("^I[0-9a-f]{8,}.*$")) {
+            final String errMsg =
+                "missing or invalid Change-Id line format in commit message footer";
+            messages.add(getFixedCommitMsgWithChangeId(errMsg, receiveEvent.commit,
+                currentUser, canonicalWebUrl, sshInfo));
+            throw new CommitValidationException(errMsg, messages);
+          }
+        }
+      }
+      return Collections.<CommitValidationMessage>emptyList();
+    }
+  }
+
+  /**
+   * If this is the special project configuration branch, validate the config.
+   */
+  public static class ConfigValidator implements CommitValidationListener {
+    private final RefControl refControl;
+    private final Repository repo;
+
+    public ConfigValidator(RefControl refControl, Repository repo) {
+      this.refControl = refControl;
+      this.repo = repo;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+
+      if (GitRepositoryManager.REF_CONFIG.equals(refControl.getRefName())) {
+        List<CommitValidationMessage> messages =
+            new LinkedList<CommitValidationMessage>();
+
+        try {
+          ProjectConfig cfg =
+              new ProjectConfig(receiveEvent.project.getNameKey());
+          cfg.load(repo, receiveEvent.command.getNewId());
+          if (!cfg.getValidationErrors().isEmpty()) {
+            addError("Invalid project configuration:", messages);
+            for (ValidationError err : cfg.getValidationErrors()) {
+              addError("  " + err.getMessage(), messages);
+            }
+            throw new ConfigInvalidException("invalid project configuration");
+          }
+        } catch (Exception e) {
+          log.error("User " + currentUser.getUserName()
+              + " tried to push invalid project configuration "
+              + receiveEvent.command.getNewId().name() + " for "
+              + receiveEvent.project.getName(), e);
+          throw new CommitValidationException("invalid project configuration",
+              messages);
+        }
+      }
+      return Collections.<CommitValidationMessage>emptyList();
+    }
+  }
+
+  /** Require permission to upload merges. */
+  public static class UploadMergesPermissionValidator implements
+      CommitValidationListener {
+    private final RefControl refControl;
+
+    public UploadMergesPermissionValidator(RefControl refControl) {
+      this.refControl = refControl;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      if (receiveEvent.commit.getParentCount() > 1
+          && !refControl.canUploadMerges()) {
+        throw new CommitValidationException("you are not allowed to upload merges");
+      }
+      return Collections.<CommitValidationMessage>emptyList();
+    }
+  }
+
+  /** Execute commit validation plug-ins */
+  public static class PluginCommitValidationListener implements
+      CommitValidationListener {
+    private final DynamicSet<CommitValidationListener> commitValidationListeners;
+
+    public PluginCommitValidationListener(
+        final DynamicSet<CommitValidationListener> commitValidationListeners) {
+      this.commitValidationListeners = commitValidationListeners;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      List<CommitValidationMessage> messages =
+          new LinkedList<CommitValidationMessage>();
+
+      for (CommitValidationListener validator : commitValidationListeners) {
+        try {
+          messages.addAll(validator.onCommitReceived(receiveEvent));
+        } catch (CommitValidationException e) {
+          messages.addAll(e.getMessages());
+          throw new CommitValidationException(e.getMessage(), messages);
+        }
+      }
+      return messages;
+    }
+  }
+
+  public static class SignedOffByValidator implements CommitValidationListener {
+    private final RefControl refControl;
+
+    public SignedOffByValidator(RefControl refControl, String canonicalWebUrl) {
+      this.refControl = refControl;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      final PersonIdent committer = receiveEvent.commit.getCommitterIdent();
+      final PersonIdent author = receiveEvent.commit.getAuthorIdent();
+      final ProjectControl projectControl = refControl.getProjectControl();
+
+      if (projectControl.getProjectState().isUseSignedOffBy()) {
+        boolean sboAuthor = false, sboCommitter = false, sboMe = false;
+        for (final FooterLine footer : receiveEvent.commit.getFooterLines()) {
+          if (footer.matches(FooterKey.SIGNED_OFF_BY)) {
+            final String e = footer.getEmailAddress();
+            if (e != null) {
+              sboAuthor |= author.getEmailAddress().equals(e);
+              sboCommitter |= committer.getEmailAddress().equals(e);
+              sboMe |= currentUser.getEmailAddresses().contains(e);
+            }
+          }
+        }
+        if (!sboAuthor && !sboCommitter && !sboMe
+            && !refControl.canForgeCommitter()) {
+          throw new CommitValidationException(
+              "not Signed-off-by author/committer/uploader in commit message footer");
+        }
+      }
+      return Collections.<CommitValidationMessage>emptyList();
+    }
+  }
+
+  /** Require that author matches the uploader. */
+  public static class AuthorUploaderValidator implements
+      CommitValidationListener {
+    private final RefControl refControl;
+    private final String canonicalWebUrl;
+
+    public AuthorUploaderValidator(RefControl refControl, String canonicalWebUrl) {
+      this.refControl = refControl;
+      this.canonicalWebUrl = canonicalWebUrl;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      final PersonIdent author = receiveEvent.commit.getAuthorIdent();
+
+      if (!currentUser.getEmailAddresses().contains(author.getEmailAddress())
+          && !refControl.canForgeAuthor()) {
+        List<CommitValidationMessage> messages =
+            new LinkedList<CommitValidationMessage>();
+
+        messages.add(getInvalidEmailError(receiveEvent.commit, "author", author,
+            currentUser, canonicalWebUrl));
+        throw new CommitValidationException("invalid author", messages);
+      }
+      return Collections.<CommitValidationMessage>emptyList();
+    }
+  }
+
+  /** Require that committer matches the uploader. */
+  public static class CommitterUploaderValidator implements
+      CommitValidationListener {
+    private final RefControl refControl;
+    private final String canonicalWebUrl;
+
+    public CommitterUploaderValidator(RefControl refControl,
+        String canonicalWebUrl) {
+      this.refControl = refControl;
+      this.canonicalWebUrl = canonicalWebUrl;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      final PersonIdent committer = receiveEvent.commit.getCommitterIdent();
+      if (!currentUser.getEmailAddresses()
+          .contains(committer.getEmailAddress())
+          && !refControl.canForgeCommitter()) {
+        List<CommitValidationMessage> messages =
+            new LinkedList<CommitValidationMessage>();
+        messages.add(getInvalidEmailError(receiveEvent.commit, "committer", committer,
+            currentUser, canonicalWebUrl));
+        throw new CommitValidationException("invalid committer", messages);
+      }
+      return Collections.<CommitValidationMessage>emptyList();
+    }
+  }
+
+  /**
+   * Don't allow the user to amend a merge created by Gerrit Code Review. This
+   * seems to happen all too often, due to users not paying any attention to
+   * what they are doing.
+   */
+  public static class AmendedGerritMergeCommitValidationListener implements
+      CommitValidationListener {
+    private final PersonIdent gerritIdent;
+    private final RefControl refControl;
+
+    public AmendedGerritMergeCommitValidationListener(
+        final RefControl refControl, final PersonIdent gerritIdent) {
+      this.refControl = refControl;
+      this.gerritIdent = gerritIdent;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      final PersonIdent author = receiveEvent.commit.getAuthorIdent();
+
+      if (receiveEvent.commit.getParentCount() > 1
+          && author.getName().equals(gerritIdent.getName())
+          && author.getEmailAddress().equals(gerritIdent.getEmailAddress())
+          && !refControl.canForgeGerritServerIdentity()) {
+        throw new CommitValidationException("do not amend merges not made by you");
+      }
+      return Collections.<CommitValidationMessage>emptyList();
+    }
+  }
+
+  private static CommitValidationMessage getInvalidEmailError(RevCommit c, String type,
+      PersonIdent who, IdentifiedUser currentUser, String canonicalWebUrl) {
+    StringBuilder sb = new StringBuilder();
+    sb.append("\n");
+    sb.append("ERROR:  In commit " + c.name() + "\n");
+    sb.append("ERROR:  " + type + " email address " + who.getEmailAddress()
+        + "\n");
+    sb.append("ERROR:  does not match your user account.\n");
+    sb.append("ERROR:\n");
+    if (currentUser.getEmailAddresses().isEmpty()) {
+      sb.append("ERROR:  You have not registered any email addresses.\n");
+    } else {
+      sb.append("ERROR:  The following addresses are currently registered:\n");
+      for (String address : currentUser.getEmailAddresses()) {
+        sb.append("ERROR:    " + address + "\n");
+      }
+    }
+    sb.append("ERROR:\n");
+    if (canonicalWebUrl != null) {
+      sb.append("ERROR:  To register an email address, please visit:\n");
+      sb.append("ERROR:  " + canonicalWebUrl + "#" + PageLinks.SETTINGS_CONTACT
+          + "\n");
+    }
+    sb.append("\n");
+    return new CommitValidationMessage(sb.toString(), false);
+  }
+
+  /**
+   * We handle 3 cases:
+   * 1. No change id in the commit message at all.
+   * 2. Change id last in the commit message but missing empty line to create the footer.
+   * 3. There is a change-id somewhere in the commit message, but we ignore it.
+   *
+   * @return The fixed up commit message
+   */
+  private static CommitValidationMessage getFixedCommitMsgWithChangeId(final String errMsg,
+      final RevCommit c, final IdentifiedUser currentUser,
+      String canonicalWebUrl, final SshInfo sshInfo) {
+    final String changeId = "Change-Id:";
+    StringBuilder sb = new StringBuilder();
+    sb.append("ERROR: ").append(errMsg);
+    sb.append('\n');
+    sb.append("Suggestion for commit message:\n");
+
+    if (c.getFullMessage().indexOf(changeId) == -1) {
+      sb.append(c.getFullMessage());
+      sb.append('\n');
+      sb.append(changeId).append(" I").append(c.name());
+    } else {
+      String lines[] = c.getFullMessage().trim().split("\n");
+      String lastLine = lines.length > 0 ? lines[lines.length - 1] : "";
+
+      if (lastLine.indexOf(changeId) == 0) {
+        for (int i = 0; i < lines.length - 1; i++) {
+          sb.append(lines[i]);
+          sb.append('\n');
+        }
+
+        sb.append('\n');
+        sb.append(lastLine);
+      } else {
+        sb.append(c.getFullMessage());
+        sb.append('\n');
+        sb.append(changeId).append(" I").append(c.name());
+        sb.append('\n');
+        sb.append("Hint: A potential Change-Id was found, but it was not in the footer of the commit message.");
+      }
+    }
+    sb.append('\n');
+    sb.append('\n');
+    sb.append("Hint: To automatically insert Change-Id, install the hook:\n");
+    sb.append(getCommitMessageHookInstallationHint(currentUser,
+        canonicalWebUrl, sshInfo)).append('\n');
+    sb.append('\n');
+
+    return new CommitValidationMessage(sb.toString(), false);
+  }
+
+  private static String getCommitMessageHookInstallationHint(
+      final IdentifiedUser currentUser, String canonicalWebUrl,
+      final SshInfo sshInfo) {
+    final List<HostKey> hostKeys = sshInfo.getHostKeys();
+
+    // If there are no SSH keys, the commit-msg hook must be installed via
+    // HTTP(S)
+    if (hostKeys.isEmpty()) {
+      String p = ".git/hooks/commit-msg";
+      return String.format(
+          "  curl -o %s %s/tools/hooks/commit-msg ; chmod +x %s", p,
+          getGerritUrl(canonicalWebUrl), p);
+    }
+
+    // SSH keys exist, so the hook can be installed with scp.
+    String sshHost;
+    int sshPort;
+    String host = hostKeys.get(0).getHost();
+    int c = host.lastIndexOf(':');
+    if (0 <= c) {
+      if (host.startsWith("*:")) {
+        sshHost = getGerritHost(canonicalWebUrl);
+      } else {
+        sshHost = host.substring(0, c);
+      }
+      sshPort = Integer.parseInt(host.substring(c + 1));
+    } else {
+      sshHost = host;
+      sshPort = 22;
+    }
+
+    return String.format("  scp -p -P %d %s@%s:hooks/commit-msg .git/hooks/",
+        sshPort, currentUser.getUserName(), sshHost);
+  }
+
+  /**
+   * Get the Gerrit URL.
+   *
+   * @return the canonical URL (with any trailing slash removed) if it is
+   *         configured, otherwise fall back to "http://hostname" where hostname
+   *         is the value returned by {@link #getGerritHost()}.
+   */
+  private static String getGerritUrl(String canonicalWebUrl) {
+    if (canonicalWebUrl != null) {
+      if (canonicalWebUrl.endsWith("/")) {
+        return canonicalWebUrl.substring(0, canonicalWebUrl.lastIndexOf("/"));
+      }
+      return canonicalWebUrl;
+    } else {
+      return "http://" + getGerritHost(canonicalWebUrl);
+    }
+  }
+
+  /**
+   * Get the Gerrit hostname.
+   *
+   * @return the hostname from the canonical URL if it is configured, otherwise
+   *         whatever the OS says the hostname is.
+   */
+  private static String getGerritHost(String canonicalWebUrl) {
+    String host;
+    if (canonicalWebUrl != null) {
+      try {
+        host = new URL(canonicalWebUrl).getHost();
+      } catch (MalformedURLException e) {
+        host = SystemReader.getInstance().getHostname();
+      }
+    } else {
+      host = SystemReader.getInstance().getHostname();
+    }
+    return host;
+  }
+
+  private static void addError(String error,
+      List<CommitValidationMessage> messages) {
+    messages.add(new CommitValidationMessage(error, true));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java
new file mode 100644
index 0000000..f7c7fb8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2013 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.group;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.util.Url;
+
+class GetGroup implements RestReadView<GroupResource> {
+
+  @Override
+  public Object apply(GroupResource resource) throws AuthException,
+      BadRequestException, ResourceConflictException, Exception {
+    GroupDescription.Basic group = resource.getControl().getGroup();
+    GroupInfo info = new GroupInfo();
+    info.name = resource.getName();
+    info.uuid = resource.getGroupUUID().get();
+    info.isVisibleToAll = group.isVisibleToAll();
+    if (group instanceof GroupDescription.Internal) {
+      final AccountGroup internalGroup =
+          ((GroupDescription.Internal) group).getAccountGroup();
+      info.description = Strings.emptyToNull(internalGroup.getDescription());
+      info.ownerUuid = internalGroup.getOwnerGroupUUID().get();
+    }
+    info.finish();
+    return info;
+  }
+
+  static class GroupInfo {
+    final String kind = "gerritcodereview#group";
+    String id;
+    String name;
+    String uuid;
+    String description;
+    boolean isVisibleToAll;
+    String ownerUuid;
+
+    void finish() {
+      id = Url.encode(GroupsCollection.UUID_PREFIX + uuid);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java
new file mode 100644
index 0000000..e7ec85c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2013 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.group;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.inject.TypeLiteral;
+
+public class GroupResource implements RestResource {
+  public static final TypeLiteral<RestView<GroupResource>> GROUP_KIND =
+      new TypeLiteral<RestView<GroupResource>>() {};
+
+  private final GroupControl control;
+
+  GroupResource(GroupControl control) {
+    this.control = control;
+  }
+
+  public String getName() {
+    return control.getGroup().getName();
+  }
+
+  public AccountGroup.UUID getGroupUUID() {
+    return control.getGroup().getGroupUUID();
+  }
+
+  public GroupControl getControl() {
+    return control;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
new file mode 100644
index 0000000..c085521
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2013 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.group;
+
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.util.Url;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class GroupsCollection implements
+    RestCollection<TopLevelResource, GroupResource> {
+  public final static String UUID_PREFIX = "uuid-";
+
+  private final DynamicMap<RestView<GroupResource>> views;
+  private final Provider<ListGroups> list;
+  private final GroupControl.Factory groupControlFactory;
+  private final Provider<CurrentUser> self;
+
+  @Inject
+  GroupsCollection(final DynamicMap<RestView<GroupResource>> views,
+      final Provider<ListGroups> list,
+      final GroupControl.Factory groupControlFactory,
+      final Provider<CurrentUser> self) {
+    this.views = views;
+    this.list = list;
+    this.groupControlFactory = groupControlFactory;
+    this.self = self;
+  }
+
+  @Override
+  public RestView<TopLevelResource> list() throws ResourceNotFoundException,
+      AuthException {
+    final CurrentUser user = self.get();
+    if (user instanceof AnonymousUser) {
+      throw new AuthException("Authentication required");
+    } else if(!(user instanceof IdentifiedUser)) {
+      throw new ResourceNotFoundException();
+    }
+
+    return list.get();
+  }
+
+  @Override
+  public GroupResource parse(TopLevelResource parent, String id)
+      throws ResourceNotFoundException, Exception {
+    final CurrentUser user = self.get();
+    if (user instanceof AnonymousUser) {
+      throw new AuthException("Authentication required");
+    } else if(!(user instanceof IdentifiedUser)) {
+      throw new ResourceNotFoundException(id);
+    }
+
+    final String decodedId = Url.decode(id);
+    final GroupControl ctl;
+    try {
+      if (decodedId.startsWith(UUID_PREFIX)) {
+        final String uuid = decodedId.substring(UUID_PREFIX.length());
+        ctl = groupControlFactory.controlFor(new AccountGroup.UUID(uuid));
+      } else {
+        try {
+          ctl = groupControlFactory.controlFor(
+              new AccountGroup.Id(Integer.parseInt(decodedId)));
+        } catch (NumberFormatException e) {
+          throw new ResourceNotFoundException(id);
+        }
+      }
+    } catch (NoSuchGroupException e) {
+      throw new ResourceNotFoundException(id);
+    }
+    if (!ctl.isVisible() && !ctl.isOwner()) {
+      throw new ResourceNotFoundException(id);
+    }
+    return new GroupResource(ctl);
+  }
+
+  @Override
+  public DynamicMap<RestView<GroupResource>> views() {
+    return views;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
new file mode 100644
index 0000000..ec03f3a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
@@ -0,0 +1,186 @@
+// Copyright (C) 2013 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.group;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.VisibleGroups;
+import com.google.gerrit.server.ioutil.ColumnFormatter;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.util.Url;
+import com.google.gson.JsonElement;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Option;
+
+import java.io.BufferedWriter;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/** List groups visible to the calling user. */
+public class ListGroups implements RestReadView<TopLevelResource> {
+
+  private final GroupCache groupCache;
+  private final VisibleGroups.Factory visibleGroupsFactory;
+  private final IdentifiedUser.GenericFactory userFactory;
+
+  @Option(name = "--project", aliases = {"-p"},
+      usage = "projects for which the groups should be listed")
+  private final List<ProjectControl> projects = new ArrayList<ProjectControl>();
+
+  @Option(name = "--visible-to-all", usage = "to list only groups that are visible to all registered users")
+  private boolean visibleToAll;
+
+  @Option(name = "--type", usage = "type of group")
+  private AccountGroup.Type groupType;
+
+  @Option(name = "--user", aliases = {"-u"},
+      usage = "user for which the groups should be listed")
+  private Account.Id user;
+
+  @Option(name = "--verbose", aliases = {"-v"},
+      usage = "verbose output format with tab-separated columns for the " +
+          "group name, UUID, description, type, owner group name, " +
+          "owner group UUID, and whether the group is visible to all")
+  private boolean verboseOutput;
+
+  @Option(name = "-m", metaVar = "MATCH", usage = "match group substring")
+  private String matchSubstring;
+
+  @Inject
+  protected ListGroups(final GroupCache groupCache,
+      final VisibleGroups.Factory visibleGroupsFactory,
+      final IdentifiedUser.GenericFactory userFactory) {
+    this.groupCache = groupCache;
+    this.visibleGroupsFactory = visibleGroupsFactory;
+    this.userFactory = userFactory;
+  }
+
+  public Account.Id getUser() {
+    return user;
+  }
+
+  public List<ProjectControl> getProjects() {
+    return projects;
+  }
+
+  @Override
+  public Object apply(TopLevelResource resource) throws AuthException,
+      BadRequestException, ResourceConflictException, Exception {
+    return display(null);
+  }
+
+  public JsonElement display(OutputStream displayOutputStream)
+      throws NoSuchGroupException {
+    PrintWriter stdout = null;
+    if (displayOutputStream != null) {
+      try {
+        stdout = new PrintWriter(new BufferedWriter(
+            new OutputStreamWriter(displayOutputStream, "UTF-8")));
+      } catch (UnsupportedEncodingException e) {
+        throw new RuntimeException("JVM lacks UTF-8 encoding", e);
+      }
+    }
+
+    try {
+      final VisibleGroups visibleGroups = visibleGroupsFactory.create();
+      visibleGroups.setOnlyVisibleToAll(visibleToAll);
+      visibleGroups.setGroupType(groupType);
+      visibleGroups.setMatch(matchSubstring);
+      final List<AccountGroup> groupList;
+      if (!projects.isEmpty()) {
+        groupList = visibleGroups.get(projects);
+      } else if (user != null) {
+        groupList = visibleGroups.get(userFactory.create(user));
+      } else {
+        groupList = visibleGroups.get();
+      }
+
+      if (stdout == null) {
+        final Map<String, GroupInfo> output = Maps.newTreeMap();
+        for (final AccountGroup g : groupList) {
+          final GroupInfo info = new GroupInfo();
+          info.name = g.getName();
+          info.groupId = g.getId().get();
+          info.setUuid(g.getGroupUUID());
+          info.description = g.getDescription();
+          info.isVisibleToAll = g.isVisibleToAll();
+          info.ownerUuid = g.getOwnerGroupUUID().get();
+          output.put(info.name, info);
+        }
+        return OutputFormat.JSON.newGson().toJsonTree(output,
+            new TypeToken<Map<String, GroupInfo>>() {}.getType());
+      } else {
+        final ColumnFormatter formatter = new ColumnFormatter(stdout, '\t');
+        for (final AccountGroup g : groupList) {
+          formatter.addColumn(g.getName());
+          if (verboseOutput) {
+            formatter.addColumn(KeyUtil.decode(g.getGroupUUID().toString()));
+            formatter.addColumn(
+                g.getDescription() != null ? g.getDescription() : "");
+            formatter.addColumn(g.getType().toString());
+            final AccountGroup owningGroup =
+                groupCache.get(g.getOwnerGroupUUID());
+            formatter.addColumn(
+                owningGroup != null ? owningGroup.getName() : "n/a");
+            formatter.addColumn(KeyUtil.decode(g.getOwnerGroupUUID().toString()));
+            formatter.addColumn(Boolean.toString(g.isVisibleToAll()));
+          }
+          formatter.nextLine();
+        }
+        formatter.finish();
+        return null;
+      }
+    } finally {
+      if (stdout != null) {
+        stdout.flush();
+      }
+    }
+  }
+
+  static class GroupInfo {
+    final String kind = "gerritcodereview#group";
+
+    transient String name;
+    String id;
+    String uuid;
+    int groupId;
+    String description;
+    boolean isVisibleToAll;
+    String ownerUuid;
+
+    void setUuid(AccountGroup.UUID u) {
+      uuid = u.get();
+      id = Url.encode(GroupsCollection.UUID_PREFIX + uuid);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java
new file mode 100644
index 0000000..58f1506
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 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.group;
+
+import static com.google.gerrit.server.group.GroupResource.GROUP_KIND;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+
+public class Module extends RestApiModule {
+  @Override
+  protected void configure() {
+    bind(GroupsCollection.class);
+
+    DynamicMap.mapOf(binder(), GROUP_KIND);
+
+    get(GROUP_KIND).to(GetGroup.class);
+  }
+}
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 f18154b..2fcdb2c 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
@@ -21,7 +21,7 @@
 import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
@@ -60,12 +60,13 @@
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Queue;
 import java.util.Set;
 import java.util.TreeSet;
 
 /** Sends an email to one or more interested parties. */
-public abstract class ChangeEmail extends OutgoingEmail {
+public abstract class ChangeEmail extends NotificationEmail {
   private static final Logger log = LoggerFactory.getLogger(ChangeEmail.class);
 
   protected final Change change;
@@ -80,7 +81,7 @@
 
   protected ChangeEmail(EmailArguments ea, final String anonymousCowardName,
       final Change c, final String mc) {
-    super(ea, anonymousCowardName, mc);
+    super(ea, anonymousCowardName, mc, c.getProject(), c.getDest());
     change = c;
     changeData = new ChangeData(change);
     emailOnlyAuthors = false;
@@ -361,8 +362,8 @@
 
     for (AccountProjectWatch w : args.db.get().accountProjectWatches()
         .byProject(change.getProject())) {
-      projectWatchers.add(w.getAccountId());
       if (w.isNotify(type)) {
+        projectWatchers.add(w.getAccountId());
         add(matching, w);
       }
     }
@@ -477,10 +478,15 @@
           .byGroup(next)) {
         matching.accounts.add(m.getAccountId());
       }
-      for (AccountGroupInclude m : args.db.get().accountGroupIncludes()
+      for (AccountGroupIncludeByUuid m : args.db.get().accountGroupIncludesByUuid()
           .byGroup(next)) {
-        if (seen.add(m.getIncludeId())) {
-          scan.add(m.getIncludeId());
+        List<AccountGroup> incGroup = args.db.get().accountGroups().
+            byUUID(m.getIncludeUUID()).toList();
+        if (incGroup.size() == 1) {
+          AccountGroup.Id includeId = incGroup.get(0).getId();
+          if (seen.add(includeId)) {
+            scan.add(includeId);
+          }
         }
       }
     }
@@ -571,10 +577,7 @@
     velocityContext.put("change", change);
     velocityContext.put("changeId", change.getKey());
     velocityContext.put("coverLetter", getCoverLetter());
-    velocityContext.put("branch", change.getDest());
     velocityContext.put("fromName", getNameFor(fromId));
-    velocityContext.put("projectName", //
-        projectState != null ? projectState.getProject().getName() : null);
     velocityContext.put("patchSet", patchSet);
     velocityContext.put("patchSetInfo", patchSetInfo);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index 6931add..656a412 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -15,10 +15,11 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.server.change.PostReview.NotifyHandling;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.patch.PatchFile;
 import com.google.gerrit.server.patch.PatchList;
@@ -38,15 +39,19 @@
 /** Send comments, after the author of them hit used Publish Comments in the UI. */
 public class CommentSender extends ReplyToChangeSender {
   public static interface Factory {
-    public CommentSender create(Change change);
+    public CommentSender create(NotifyHandling notify, Change change);
   }
 
+  private final NotifyHandling notify;
   private List<PatchLineComment> inlineComments = Collections.emptyList();
 
   @Inject
   public CommentSender(EmailArguments ea,
-      @AnonymousCowardName String anonymousCowardName, @Assisted Change c) {
+      @AnonymousCowardName String anonymousCowardName,
+      @Assisted NotifyHandling notify,
+      @Assisted Change c) {
     super(ea, anonymousCowardName, c, "comment");
+    this.notify = notify;
   }
 
   public void setPatchLineComments(final List<PatchLineComment> plc) {
@@ -68,9 +73,13 @@
   protected void init() throws EmailException {
     super.init();
 
-    ccAllApprovals();
-    bccStarredBy();
-    includeWatchers(NotifyType.ALL_COMMENTS);
+    if (notify.compareTo(NotifyHandling.OWNER_REVIEWERS) >= 0) {
+      ccAllApprovals();
+    }
+    if (notify.compareTo(NotifyHandling.ALL) >= 0) {
+      bccStarredBy();
+      includeWatchers(NotifyType.ALL_COMMENTS);
+    }
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
new file mode 100644
index 0000000..f22c6e4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
@@ -0,0 +1,131 @@
+// Copyright (C) 2013 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.mail;
+
+import com.google.gerrit.common.errors.NoSuchAccountException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gwtorm.server.OrmException;
+
+import org.eclipse.jgit.revwalk.FooterKey;
+import org.eclipse.jgit.revwalk.FooterLine;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class MailUtil {
+  private static final FooterKey REVIEWED_BY = new FooterKey("Reviewed-by");
+  private static final FooterKey TESTED_BY = new FooterKey("Tested-by");
+
+  public static MailRecipients getRecipientsFromFooters(
+      final AccountResolver accountResolver, final PatchSet ps,
+      final List<FooterLine> footerLines) throws OrmException {
+    final MailRecipients recipients = new MailRecipients();
+    if (!ps.isDraft()) {
+      for (final FooterLine footerLine : footerLines) {
+        try {
+          if (isReviewer(footerLine)) {
+            recipients.reviewers.add(toAccountId(accountResolver, footerLine
+                .getValue().trim()));
+          } else if (footerLine.matches(FooterKey.CC)) {
+            recipients.cc.add(toAccountId(accountResolver, footerLine
+                .getValue().trim()));
+          }
+        } catch (NoSuchAccountException e) {
+          continue;
+        }
+      }
+    }
+    return recipients;
+  }
+
+  public static MailRecipients getRecipientsFromApprovals(
+      final List<PatchSetApproval> approvals) {
+    final MailRecipients recipients = new MailRecipients();
+    for (PatchSetApproval a : approvals) {
+      if (a.getValue() != 0) {
+        recipients.reviewers.add(a.getAccountId());
+      } else {
+        recipients.cc.add(a.getAccountId());
+      }
+    }
+    return recipients;
+  }
+
+  private static Account.Id toAccountId(final AccountResolver accountResolver,
+      final String nameOrEmail) throws OrmException, NoSuchAccountException {
+    final Account a = accountResolver.findByNameOrEmail(nameOrEmail);
+    if (a == null) {
+      throw new NoSuchAccountException("\"" + nameOrEmail
+          + "\" is not registered");
+    }
+    return a.getId();
+  }
+
+  private static boolean isReviewer(final FooterLine candidateFooterLine) {
+    return candidateFooterLine.matches(FooterKey.SIGNED_OFF_BY)
+        || candidateFooterLine.matches(FooterKey.ACKED_BY)
+        || candidateFooterLine.matches(REVIEWED_BY)
+        || candidateFooterLine.matches(TESTED_BY);
+  }
+
+  public static class MailRecipients {
+    private final Set<Account.Id> reviewers;
+    private final Set<Account.Id> cc;
+
+    public MailRecipients() {
+      this.reviewers = new HashSet<Account.Id>();
+      this.cc = new HashSet<Account.Id>();
+    }
+
+    public MailRecipients(final Set<Account.Id> reviewers,
+        final Set<Account.Id> cc) {
+      this.reviewers = new HashSet<Account.Id>(reviewers);
+      this.cc = new HashSet<Account.Id>(cc);
+    }
+
+    public void add(final MailRecipients recipients) {
+      reviewers.addAll(recipients.reviewers);
+      cc.addAll(recipients.cc);
+    }
+
+    public void remove(final Account.Id toRemove) {
+      reviewers.remove(toRemove);
+      cc.remove(toRemove);
+    }
+
+    public Set<Account.Id> getReviewers() {
+      return Collections.unmodifiableSet(reviewers);
+    }
+
+    public Set<Account.Id> getCcOnly() {
+      final Set<Account.Id> cc = new HashSet<Account.Id>(this.cc);
+      cc.removeAll(reviewers);
+      return Collections.unmodifiableSet(cc);
+    }
+
+    public Set<Account.Id> getAll() {
+      final Set<Account.Id> all =
+          new HashSet<Account.Id>(reviewers.size() + cc.size());
+      all.addAll(reviewers);
+      all.addAll(cc);
+      return Collections.unmodifiableSet(all);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java
new file mode 100644
index 0000000..b0d5862
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2012 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.mail;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+
+/**
+ * Common class for notifications that are related to a project and branch
+ */
+public abstract class NotificationEmail extends OutgoingEmail {
+  protected Project.NameKey project;
+  protected Branch.NameKey branch;
+
+  protected NotificationEmail(EmailArguments ea, String anonymousCowardName,
+      String mc, Project.NameKey project, Branch.NameKey branch) {
+    super(ea, anonymousCowardName, mc);
+
+    this.project = project;
+    this.branch = branch;
+  }
+
+  @Override
+  protected void setupVelocityContext() {
+    super.setupVelocityContext();
+    velocityContext.put("projectName", project.get());
+    velocityContext.put("branch", branch);
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
index 7018a3b..50efb68 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
@@ -39,7 +39,9 @@
     } catch (IOException err) {
     }
     if (!tmpFile.delete() && tmpFile.exists()) {
-      PluginLoader.log.warn("Cannot delete " + tmpFile.getAbsolutePath());
+      PluginLoader.log.warn("Cannot delete " + tmpFile.getAbsolutePath()
+          + ", retrying to delete it on termination of the virtual machine");
+      tmpFile.deleteOnExit();
     } else {
       PluginLoader.log.info("Cleaned plugin " + tmpFile.getName());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index 3502b00..4e565d3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -306,6 +306,10 @@
     dropRemovedDisabledPlugins(jars);
 
     for (File jar : jars) {
+      if (jar.getName().endsWith(".disabled")) {
+        continue;
+      }
+
       String name = nameOf(jar);
       FileSnapshot brokenTime = broken.get(name);
       if (brokenTime != null && !brokenTime.isModified(jar)) {
@@ -447,9 +451,10 @@
       URL[] urls = {tmp.toURI().toURL()};
       ClassLoader parentLoader = parentFor(type);
       ClassLoader pluginLoader = new URLClassLoader(urls, parentLoader);
-      cleanupHandles.put(
-          new CleanupHandle(tmp, jarFile, pluginLoader, cleanupQueue),
-          Boolean.TRUE);
+      final CleanupHandle cleanupHandle =
+          new CleanupHandle(tmp, jarFile, pluginLoader, cleanupQueue);
+      cleanupHandle.enqueue();
+      cleanupHandles.put(cleanupHandle, Boolean.TRUE);
 
       Class<? extends Module> sysModule = load(sysName, pluginLoader);
       Class<? extends Module> sshModule = load(sshName, pluginLoader);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 3dbd7b7..06bc099 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -205,8 +205,6 @@
     projectCache.onCreateProject(createProjectArgs.getProject());
     repoManager.setProjectDescription(createProjectArgs.getProject(),
         createProjectArgs.projectDescription);
-    referenceUpdated.fire(createProjectArgs.getProject(),
-        GitRepositoryManager.REF_CONFIG);
   }
 
   private void validateParameters() throws ProjectCreationFailedException {
@@ -216,11 +214,18 @@
     }
 
     if (createProjectArgs.getProjectName().endsWith(Constants.DOT_GIT_EXT)) {
-      createProjectArgs.setProjectName(createProjectArgs.getProjectName()
-          .substring(
-              0,
-              createProjectArgs.getProjectName().length()
-                  - Constants.DOT_GIT_EXT.length()));
+      String nameWithoutSuffix = createProjectArgs.getProjectName();
+      // Be nice and drop the trailing ".git" suffix, which we never keep
+      // in our database, but clients might mistakenly provide anyway.
+      //
+      nameWithoutSuffix = nameWithoutSuffix.substring(0, //
+          nameWithoutSuffix.length() - Constants.DOT_GIT_EXT.length());
+      while (nameWithoutSuffix.endsWith("/")) {
+        nameWithoutSuffix =
+            nameWithoutSuffix.substring(0, nameWithoutSuffix.length() - 1);
+      }
+
+      createProjectArgs.setProjectName(nameWithoutSuffix);
     }
 
     if (!currentUser.getCapabilities().canCreateProject()) {
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 89e6ee6..1019d33 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. */
-  public static final Class<Schema_73> C = Schema_73.class;
+  public static final Class<Schema_74> C = Schema_74.class;
 
   public static class Module extends AbstractModule {
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
index 3a288e2..2ab83c8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -41,6 +42,8 @@
 import org.eclipse.jgit.util.FS;
 
 import java.io.IOException;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
 import java.util.Collections;
 
 public class Schema_57 extends SchemaVersion {
@@ -95,6 +98,11 @@
 
         // Move the repository.*.createGroup to Create Project.
         String[] createGroupList = cfg.getStringList("repository", "*", "createGroup");
+
+        // Prepare the account_group_includes query
+        PreparedStatement stmt = ((JdbcSchema) db).getConnection().
+            prepareStatement("SELECT * FROM account_group_includes WHERE group_id = ?");
+
         for (String name : createGroupList) {
           AccountGroup.NameKey key = new AccountGroup.NameKey(name);
           AccountGroupName groupName = db.accountGroupNames().get(key);
@@ -117,9 +125,10 @@
         }
 
         AccountGroup batch = db.accountGroups().get(sc.batchUsersGroupId);
+        stmt.setInt(0, sc.batchUsersGroupId.get());
         if (batch != null
             && db.accountGroupMembers().byGroup(sc.batchUsersGroupId).toList().isEmpty()
-            &&  db.accountGroupIncludes().byGroup(sc.batchUsersGroupId).toList().isEmpty()) {
+            &&  stmt.executeQuery().first() != false) {
           // If the batch user group is not used, delete it.
           //
           db.accountGroups().delete(Collections.singleton(batch));
@@ -136,6 +145,8 @@
 
         md.setMessage("Upgrade to Gerrit Code Review schema 57\n");
         config.commit(md);
+      } catch (SQLException err) {
+        throw new OrmException( "Cannot read account_group_includes", err);
       } finally {
         git.close();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java
new file mode 100644
index 0000000..ca012d1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java
@@ -0,0 +1,114 @@
+// Copyright (C) 2012 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.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/* Handles copying all entries from AccountGroupIncludes(Audit) to the new tables */
+public class Schema_74 extends SchemaVersion {
+  @Inject
+  Schema_74(Provider<Schema_73> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(final ReviewDb db, final UpdateUI ui)
+      throws SQLException, OrmException {
+    // Grab all the groups since we don't have the cache available
+    HashMap<AccountGroup.Id, AccountGroup.UUID> allGroups =
+        new HashMap<AccountGroup.Id, AccountGroup.UUID>();
+    for( AccountGroup ag : db.accountGroups().all() ) {
+      allGroups.put(ag.getId(), ag.getGroupUUID());
+    }
+
+    // Initialize some variables
+    Connection conn = ((JdbcSchema) db).getConnection();
+    ArrayList<AccountGroupIncludeByUuid> newIncludes =
+        new ArrayList<AccountGroupIncludeByUuid>();
+    ArrayList<AccountGroupIncludeByUuidAudit> newIncludeAudits =
+        new ArrayList<AccountGroupIncludeByUuidAudit>();
+
+    // Iterate over all entries in account_group_includes
+    Statement oldGroupIncludesStmt = conn.createStatement();
+    ResultSet oldGroupIncludes = oldGroupIncludesStmt.
+        executeQuery("SELECT * FROM account_group_includes");
+    while (oldGroupIncludes.next()) {
+      AccountGroup.Id oldGroupId =
+          new AccountGroup.Id(oldGroupIncludes.getInt("group_id"));
+      AccountGroup.Id oldIncludeId =
+          new AccountGroup.Id(oldGroupIncludes.getInt("include_id"));
+      AccountGroup.UUID uuidFromIncludeId = allGroups.get(oldIncludeId);
+
+      // If we've got an include, but the group no longer exists, don't bother converting
+      if (uuidFromIncludeId == null) {
+        ui.message("Skipping group_id = \"" + oldIncludeId.get() +
+            "\", not a current group");
+        continue;
+      }
+
+      // Create the new include entry
+      AccountGroupIncludeByUuid destIncludeEntry = new AccountGroupIncludeByUuid(
+          new AccountGroupIncludeByUuid.Key(oldGroupId, uuidFromIncludeId));
+
+      // Iterate over all the audits (for this group)
+      PreparedStatement oldAuditsQuery = conn.prepareStatement(
+          "SELECT * FROM account_group_includes_audit WHERE group_id=? AND include_id=?");
+      oldAuditsQuery.setInt(1, oldGroupId.get());
+      oldAuditsQuery.setInt(2, oldIncludeId.get());
+      ResultSet oldGroupIncludeAudits = oldAuditsQuery.executeQuery();
+      while (oldGroupIncludeAudits.next()) {
+        Account.Id addedBy = new Account.Id(oldGroupIncludeAudits.getInt("added_by"));
+        int removedBy = oldGroupIncludeAudits.getInt("removed_by");
+
+        // Create the new audit entry
+        AccountGroupIncludeByUuidAudit destAuditEntry =
+            new AccountGroupIncludeByUuidAudit(destIncludeEntry, addedBy,
+                oldGroupIncludeAudits.getTimestamp("added_on"));
+
+        // If this was a "removed on" entry, note that
+        if (removedBy > 0) {
+          destAuditEntry.removed(new Account.Id(removedBy),
+              oldGroupIncludeAudits.getTimestamp("removed_on"));
+        }
+        newIncludeAudits.add(destAuditEntry);
+      }
+      newIncludes.add(destIncludeEntry);
+      oldAuditsQuery.close();
+      oldGroupIncludeAudits.close();
+    }
+    oldGroupIncludes.close();
+    oldGroupIncludesStmt.close();
+
+    // Now insert all of the new entries to the database
+    db.accountGroupIncludesByUuid().insert(newIncludes);
+    db.accountGroupIncludesByUuidAudit().insert(newIncludeAudits);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java
index 519b922..6ceec2d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java
@@ -19,7 +19,7 @@
 import java.util.Collections;
 import java.util.List;
 
-class NoSshInfo implements SshInfo {
+public class NoSshInfo implements SshInfo {
   @Override
   public List<HostKey> getHostKeys() {
     return Collections.emptyList();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 728c20c..da08395 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -57,10 +57,10 @@
   @Option(name = "--visible-to-all", usage = "to make the group visible to all registered users")
   private boolean visibleToAll;
 
-  private final Set<AccountGroup.Id> initialGroups = new HashSet<AccountGroup.Id>();
+  private final Set<AccountGroup.UUID> initialGroups = new HashSet<AccountGroup.UUID>();
 
   @Option(name = "--group", aliases = "-g", metaVar = "GROUP", usage = "initial set of groups to be included in the group")
-  void addGroup(final AccountGroup.Id id) {
+  void addGroup(final AccountGroup.UUID id) {
     initialGroups.add(id);
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index f8856f2..412ea70 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -14,93 +14,27 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.common.data.GroupList;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.VisibleGroups;
-import com.google.gerrit.server.ioutil.ColumnFormatter;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.client.KeyUtil;
+import com.google.gerrit.server.group.ListGroups;
+import com.google.gerrit.sshd.BaseCommand;
 import com.google.inject.Inject;
 
-import org.kohsuke.args4j.Option;
+import org.apache.sshd.server.Environment;
 
-import java.util.ArrayList;
-import java.util.List;
-
-public class ListGroupsCommand extends SshCommand {
+public class ListGroupsCommand extends BaseCommand {
   @Inject
-  private GroupCache groupCache;
-
-  @Inject
-  private VisibleGroups.Factory visibleGroupsFactory;
-
-  @Inject
-  private IdentifiedUser.GenericFactory userFactory;
-
-  @Option(name = "--project", aliases = {"-p"},
-      usage = "projects for which the groups should be listed")
-  private final List<ProjectControl> projects = new ArrayList<ProjectControl>();
-
-  @Option(name = "--visible-to-all", usage = "to list only groups that are visible to all registered users")
-  private boolean visibleToAll;
-
-  @Option(name = "--type", usage = "type of group")
-  private AccountGroup.Type groupType;
-
-  @Option(name = "--user", aliases = {"-u"},
-      usage = "user for which the groups should be listed")
-  private Account.Id user;
-
-  @Option(name = "--verbose", aliases = {"-v"},
-      usage = "verbose output format with tab-separated columns for the " +
-          "group name, UUID, description, type, owner group name, " +
-          "owner group UUID, and whether the group is visible to all")
-  private boolean verboseOutput;
+  private ListGroups impl;
 
   @Override
-  protected void run() throws Failure {
-    try {
-      if (user != null && !projects.isEmpty()) {
-        throw new UnloggedFailure(1, "fatal: --user and --project options are not compatible.");
-      }
-
-      final VisibleGroups visibleGroups = visibleGroupsFactory.create();
-      visibleGroups.setOnlyVisibleToAll(visibleToAll);
-      visibleGroups.setGroupType(groupType);
-      final GroupList groupList;
-      if (!projects.isEmpty()) {
-        groupList = visibleGroups.get(projects);
-      } else if (user != null) {
-        groupList = visibleGroups.get(userFactory.create(user));
-      } else {
-        groupList = visibleGroups.get();
-      }
-
-      final ColumnFormatter formatter = new ColumnFormatter(stdout, '\t');
-      for (final AccountGroup g : groupList.getGroups()) {
-        formatter.addColumn(g.getName());
-        if (verboseOutput) {
-          formatter.addColumn(KeyUtil.decode(g.getGroupUUID().toString()));
-          formatter.addColumn(
-              g.getDescription() != null ? g.getDescription() : "");
-          formatter.addColumn(g.getType().toString());
-          final AccountGroup owningGroup =
-              groupCache.get(g.getOwnerGroupUUID());
-          formatter.addColumn(
-              owningGroup != null ? owningGroup.getName() : "n/a");
-          formatter.addColumn(KeyUtil.decode(g.getOwnerGroupUUID().toString()));
-          formatter.addColumn(Boolean.toString(g.isVisibleToAll()));
+  public void start(final Environment env) {
+    startThread(new CommandRunnable() {
+      @Override
+      public void run() throws Exception {
+        parseCommandLine(impl);
+        if (impl.getUser() != null && !impl.getProjects().isEmpty()) {
+          throw new UnloggedFailure(1, "fatal: --user and --project options are not compatible.");
         }
-        formatter.nextLine();
+        impl.display(out);
       }
-      formatter.finish();
-    } catch (NoSuchGroupException e) {
-      throw die(e);
-    }
+    });
   }
 }