Merge "Allow group includes to be by UUID instead of group ID"
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 452e2cd..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
@@ -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
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-server/src/main/java/com/google/gerrit/server/mail/EmailException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/EmailException.java
similarity index 83%
rename from gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailException.java
rename to gerrit-common/src/main/java/com/google/gerrit/common/errors/EmailException.java
index a33cb63..635335d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/EmailException.java
@@ -12,16 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.mail;
+package com.google.gerrit.common.errors;
 
 public class EmailException extends Exception {
   private static final long serialVersionUID = 1L;
 
+  public static final String MESSAGE = "Mail Error: ";
+
   public EmailException(String msg) {
-    super(msg);
+    super(MESSAGE + msg);
   }
 
   public EmailException(String msg, Throwable why) {
-    super(msg, why);
+    super(MESSAGE + msg, why);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
index 1b95038..243e9ff 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritCss.java
@@ -147,6 +147,7 @@
   String leftMostCell();
   String lineHeader();
   String lineNumber();
+  String link();
   String linkMenuBar();
   String linkMenuItemNotLast();
   String menuBarUserName();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index 7032196..2afac1c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -111,6 +111,7 @@
   String buttonCancel();
   String titleRegisterNewEmail();
   String descRegisterNewEmail();
+  String errorDialogTitleRegisterNewEmail();
 
   String newAgreement();
   String agreementStatus();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index ea35c66..8d57096 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -119,6 +119,7 @@
 descRegisterNewEmail = \
   <p>A confirmation link will be sent by email to this address.</p>\
   <p>You must click on the link to complete the registration and make the address available for selection.</p>
+errorDialogTitleRegisterNewEmail = Email Registration Failed
 
 
 newAgreement = New Contributor Agreement
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index 9ac35f0..cba2f0b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -300,7 +301,15 @@
           public void onFailure(final Throwable caught) {
             inEmail.setEnabled(true);
             register.setEnabled(true);
-            super.onFailure(caught);
+            if (caught.getMessage().startsWith(EmailException.MESSAGE)) {
+              final ErrorDialog d =
+                  new ErrorDialog(caught.getMessage().substring(
+                      EmailException.MESSAGE.length()));
+              d.setText(Util.C.errorDialogTitleRegisterNewEmail());
+              d.center();
+            } else {
+              super.onFailure(caught);
+            }
           }
         });
       }
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/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/ApprovalTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
index 3891edd..058e5d0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
@@ -383,6 +383,7 @@
       remove.setTitle(Util.M.removeReviewer( //
           FormatUtil.name(accountCache.get(ad.getAccount()))));
       remove.setStyleName(Gerrit.RESOURCES.css().removeReviewer());
+      remove.addStyleName(Gerrit.RESOURCES.css().link());
       remove.addClickHandler(new ClickHandler() {
         @Override
         public void onClick(ClickEvent event) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index e3f7c97..2fd9643 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -92,6 +92,7 @@
   String changeInfoBlockProject();
   String changeInfoBlockBranch();
   String changeInfoBlockTopic();
+  String changeInfoBlockTopicAlterTopicToolTip();
   String changeInfoBlockUploaded();
   String changeInfoBlockUpdated();
   String changeInfoBlockStatus();
@@ -132,6 +133,7 @@
   String revertChangeTitle();
 
   String headingEditCommitMessage();
+  String editCommitMessageToolTip();
   String titleEditCommitMessage();
 
   String buttonAbandonChangeBegin();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index c236b18..816b623 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -69,6 +69,7 @@
 changeInfoBlockProject = Project
 changeInfoBlockBranch = Branch
 changeInfoBlockTopic = Topic
+changeInfoBlockTopicAlterTopicToolTip = Edit Topic
 changeInfoBlockUploaded = Uploaded
 changeInfoBlockUpdated = Updated
 changeInfoBlockStatus = Status
@@ -117,6 +118,7 @@
 revertChangeTitle = Code Review - Revert Merged Change
 
 headingEditCommitMessage = Commit Message
+editCommitMessageToolTip = Edit Commit Message
 titleEditCommitMessage = Create New Patch Set
 
 buttonRestoreChangeBegin = Restore Change
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
index f891990..5468f1a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
@@ -156,6 +156,8 @@
 
     if (changeDetail.canEditTopicName()) {
       final Image edit = new Image(Gerrit.RESOURCES.edit());
+      edit.addStyleName(Gerrit.RESOURCES.css().link());
+      edit.setTitle(Util.C.changeInfoBlockTopicAlterTopicToolTip());
       edit.addClickHandler(new  ClickHandler() {
         @Override
         public void onClick(final ClickEvent event) {
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/changes/CommitMessageBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
index e983b87..8d20f0c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
@@ -123,6 +123,8 @@
           false));
       if (canEditCommitMessage) {
         final Image edit = new Image(Gerrit.RESOURCES.edit());
+        edit.setTitle(Util.C.editCommitMessageToolTip());
+        edit.addStyleName(Gerrit.RESOURCES.css().link());
         edit.addClickHandler(new ClickHandler() {
           @Override
           public void onClick(final ClickEvent event) {
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 b1a780d..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
@@ -106,6 +106,9 @@
   padding: 0.2em 0.2em 0.2em 0.5em;
 }
 
+.link {
+  cursor: pointer;
+}
 
 /** MenuScreen **/
 .menuScreenMenuBar {
@@ -957,7 +960,6 @@
 
 .changeInfoTopicPanel img {
   float: right;
-  cursor: pointer;
 }
 
 .changeInfoTopicPanel a {
@@ -1156,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 3f71e76..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;
@@ -148,6 +149,7 @@
     // Prepare icons.
     iconA = new Image(Gerrit.RESOURCES.addFileComment());
     iconA.setTitle(PatchUtil.C.addFileCommentToolTip());
+    iconA.addStyleName(Gerrit.RESOURCES.css().link());
     iconA.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
@@ -156,6 +158,7 @@
     });
     iconB = new Image(Gerrit.RESOURCES.addFileComment());
     iconB.setTitle(PatchUtil.C.addFileCommentToolTip());
+    iconB.addStyleName(Gerrit.RESOURCES.css().link());
     iconB.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
@@ -235,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 b62a10b..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
@@ -18,10 +18,10 @@
 import com.google.gerrit.common.data.AccountSecurity;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.errors.ContactInformationStoreException;
+import com.google.gerrit.common.errors.EmailException;
 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;
@@ -47,7 +47,6 @@
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.contact.ContactStore;
-import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.EmailTokenVerifier;
 import com.google.gerrit.server.mail.RegisterNewEmailSender;
 import com.google.gerrit.server.project.ProjectCache;
@@ -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 67a23f0..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;
@@ -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,
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 c3e5d4f..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
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Change;
@@ -27,19 +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.mail.EmailException;
+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/changedetail/RebaseChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChangeHandler.java
index 465ba42..b47c8f2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChangeHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChangeHandler.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.httpd.rpc.changedetail;
 
 import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.changedetail.RebaseChange;
-import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
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/resources/com/google/gerrit/reviewdb/server/mysql_nextval.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/mysql_nextval.sql
deleted file mode 100644
index 2479010..0000000
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/mysql_nextval.sql
+++ /dev/null
@@ -1,14 +0,0 @@
--- Gerrit 2 : MySQL
---
-delimiter //
-
-CREATE FUNCTION nextval_account_id ()
-  RETURNS BIGINT
-  LANGUAGE SQL
-  NOT DETERMINISTIC
-  MODIFIES SQL DATA
-BEGIN
-  INSERT INTO account_id (s) VALUES (NULL);
-  RETURN LAST_INSERT_ID();
-END;
-//
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 fda2907..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
@@ -16,6 +16,7 @@
 
 import com.google.common.base.CharMatcher;
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -26,16 +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.EmailException;
 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 9141566..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;
   }
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 f613d05..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
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -33,7 +34,6 @@
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeUtil;
-import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.RebasedPatchSetSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -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/AbandonedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
index b6ad8d8..90db3d7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.config.AnonymousCowardName;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
index f7ace27..661737b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.ssh.SshInfo;
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 1274016..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
@@ -18,6 +18,7 @@
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.data.GroupReference;
+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.AccountGroupIncludeByUuid;
@@ -65,7 +66,7 @@
 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);
       }
     }
@@ -576,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 2d0046c..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
@@ -14,10 +14,12 @@
 
 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;
@@ -37,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) {
@@ -67,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/CommitMessageEditedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommitMessageEditedSender.java
index de3cb65..4e378f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommitMessageEditedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommitMessageEditedSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.ssh.SshInfo;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
index 20c2e15..1619c51 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.common.collect.Iterables;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Change;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java
index 9c4f4c4..a7a1028 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
+
 import java.util.Collection;
 import java.util.Map;
 
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/MergeFailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
index b695fb4..9612ee2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
index db56176..08d3451 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.ApprovalCategory;
 import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
index 2e459d4..bb62bac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.ssh.SshInfo;
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/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index 187b8a7..9bb087d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.common.collect.Sets;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.server.account.AccountState;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RebasedPatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RebasedPatchSetSender.java
index 8fc8238..49e08b1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RebasedPatchSetSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RebasedPatchSetSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.ssh.SshInfo;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
index 17fe9c6..923fead 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
index 9705b8a..14c4fca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.AnonymousCowardName;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
index 2a77d39..c9540c7e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 
 /** Alert a user to a reply to a change, usually commentary made during review. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
index 55884e9..f72af26 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.config.AnonymousCowardName;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
index d62e82a..8b9a6d7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.config.AnonymousCowardName;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
index f681710..bd897b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.common.Version;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.AbstractModule;
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/BaseDataSourceType.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/BaseDataSourceType.java
index 1d12fea..e72c3e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/BaseDataSourceType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/BaseDataSourceType.java
@@ -42,11 +42,6 @@
     return getScriptRunner("index_generic.sql");
   }
 
-  @Override
-  public ScriptRunner getNextValScript() throws IOException {
-    return ScriptRunner.NOOP;
-  }
-
   protected static final ScriptRunner getScriptRunner(String path) throws IOException {
     if (path == null) {
       return ScriptRunner.NOOP;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceType.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceType.java
index 513ef67..14cb780 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceType.java
@@ -33,12 +33,4 @@
    * @throws IOException
    */
   public ScriptRunner getIndexScript() throws IOException;
-
-  /**
-   * Return a ScriptRunner that runs the nextVal script. Must not return
-   * <code>null</code>, but may return a ScriptRunner that does nothing.
-   *
-   * @throws IOException
-   */
-  public ScriptRunner getNextValScript() throws IOException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java
index ed7aadf..8c46732 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java
@@ -23,8 +23,6 @@
 
 import org.eclipse.jgit.lib.Config;
 
-import java.io.IOException;
-
 class MySql extends BaseDataSourceType {
 
   private Config cfg;
@@ -51,9 +49,4 @@
   public boolean usePool() {
     return false;
   }
-
-  @Override
-  public ScriptRunner getNextValScript() throws IOException {
-    return getScriptRunner("mysql_nextval.sql");
-  }
 }
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
index 0ca5d90..1742f48 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -119,7 +119,6 @@
     }
 
     dataSourceType.getIndexScript().run(db);
-    dataSourceType.getNextValScript().run(db);
   }
 
   private AccountGroup newGroup(ReviewDb c, String name, AccountGroup.UUID uuid)
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/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);
-    }
+    });
   }
 }