Merge "Fix style sheets for expanded context lines"
diff --git a/.gitignore b/.gitignore
index f318b65..465893d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@
 /.settings/org.eclipse.jdt.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
 /test_site
+/.idea
+/gerrit-parent.iml
diff --git a/Documentation/cmd-ban-commit.txt b/Documentation/cmd-ban-commit.txt
new file mode 100644
index 0000000..fb4a2ac
--- /dev/null
+++ b/Documentation/cmd-ban-commit.txt
@@ -0,0 +1,60 @@
+gerrit ban-commit
+=================
+
+NAME
+----
+gerrit ban-commit - Bans a commit from a project's repository.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit ban-commit'
+  [--reason <REASON>]
+  <PROJECT>
+  <COMMIT> ...
+
+DESCRIPTION
+-----------
+Marks a commit as banned for the specified repository.  If a commit is
+banned Gerrit rejects every push that includes this commit with
+link:error-contains-banned-commit.html[contains banned commit ...].
+
+[NOTE]
+This command just marks the commit as banned, but it does not remove
+the commit from the history of any central branch.  This needs to be
+done manually.
+
+ACCESS
+------
+Caller must be owner of the project or be a member of the privileged
+'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<PROJECT>::
+	Required; name of the project for which the commit should be
+	banned.
+
+<COMMIT>::
+	Required; commit(s) that should be banned.
+
+--reason::
+	Reason for banning the commit.
+
+EXAMPLES
+--------
+Ban commit `421919d015c062fd28901fe144a78a555d0b5984` from project
+`myproject`:
+
+====
+	$ ssh -p 29418 review.example.com gerrit ban-commit myproject \
+	421919d015c062fd28901fe144a78a555d0b5984
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index b09c3b3..e7d59fb 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -54,6 +54,9 @@
 'gerrit approve'::
 	'Deprecated alias for `gerrit review`.'
 
+link:cmd-ban-commit.html[gerrit ban-commit]::
+	Bans a commit from a project's repository.
+
 link:cmd-ls-groups.html[gerrit ls-groups]::
 	List groups visible to the caller.
 
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 54f7752..e7cc9e4 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1898,6 +1898,12 @@
 updated versions. If false, a server restart is required to change
 any of these resources. Default is true, allowing automatic reloads.
 
+[[site.enableDeprecatedQuery]]site.enableDeprecatedQuery::
++
+If true the deprecated `/query` URL is available to return JSON
+and text results for changes. If false, the URL is disabled and
+returns 404 to clients. Default is true, enabling `/query`.
+
 
 [[sshd]] Section sshd
 ~~~~~~~~~~~~~~~~~~~~~
diff --git a/gerrit-antlr/.gitignore b/gerrit-antlr/.gitignore
index 194bedc..fb047af 100644
--- a/gerrit-antlr/.gitignore
+++ b/gerrit-antlr/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-antlr.iml
\ No newline at end of file
diff --git a/gerrit-antlr/pom.xml b/gerrit-antlr/pom.xml
index aa0d7fd..34cb46f 100644
--- a/gerrit-antlr/pom.xml
+++ b/gerrit-antlr/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-antlr</artifactId>
diff --git a/gerrit-common/.gitignore b/gerrit-common/.gitignore
index 194bedc..759f12c 100644
--- a/gerrit-common/.gitignore
+++ b/gerrit-common/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-common.iml
\ No newline at end of file
diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml
index e7933ea..9b3fe5f 100644
--- a/gerrit-common/pom.xml
+++ b/gerrit-common/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-common</artifactId>
diff --git a/gerrit-ehcache/.gitignore b/gerrit-ehcache/.gitignore
index 20251d4..fe190c9 100644
--- a/gerrit-ehcache/.gitignore
+++ b/gerrit-ehcache/.gitignore
@@ -3,3 +3,4 @@
 /.project
 /.settings/org.eclipse.m2e.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
+/gerrit-ehcache.iml
\ No newline at end of file
diff --git a/gerrit-ehcache/pom.xml b/gerrit-ehcache/pom.xml
index 839c52b0..f9117b9 100644
--- a/gerrit-ehcache/pom.xml
+++ b/gerrit-ehcache/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-ehcache</artifactId>
diff --git a/gerrit-gwtdebug/.gitignore b/gerrit-gwtdebug/.gitignore
index 194bedc..4207862 100644
--- a/gerrit-gwtdebug/.gitignore
+++ b/gerrit-gwtdebug/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-gwtdebug.iml
\ No newline at end of file
diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml
index 734f645..01b93a6 100644
--- a/gerrit-gwtdebug/pom.xml
+++ b/gerrit-gwtdebug/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtdebug</artifactId>
diff --git a/gerrit-gwtui/.gitignore b/gerrit-gwtui/.gitignore
index 194bedc..53d46b3 100644
--- a/gerrit-gwtui/.gitignore
+++ b/gerrit-gwtui/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-gwtui.iml
\ No newline at end of file
diff --git a/gerrit-gwtui/pom.xml b/gerrit-gwtui/pom.xml
index d6ac743..b3291d1 100644
--- a/gerrit-gwtui/pom.xml
+++ b/gerrit-gwtui/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtui</artifactId>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
index 3aee0e2..ffa76ed 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -58,6 +58,7 @@
 import com.google.gerrit.client.auth.userpass.UserPassSignInDialog;
 import com.google.gerrit.client.changes.AccountDashboardScreen;
 import com.google.gerrit.client.changes.ChangeScreen;
+import com.google.gerrit.client.changes.CustomDashboardScreen;
 import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.changes.PublishCommentScreen;
 import com.google.gerrit.client.changes.QueryScreen;
@@ -361,8 +362,18 @@
   }
 
   private static void dashboard(final String token) {
-    Gerrit.display(token, //
-        new AccountDashboardScreen(Account.Id.parse(skip(token))));
+    String rest = skip(token);
+    if (rest.matches("[0-9]+")) {
+      Gerrit.display(token, new AccountDashboardScreen(Account.Id.parse(rest)));
+      return;
+    }
+
+    if (rest.startsWith("?")) {
+      Gerrit.display(token, new CustomDashboardScreen(rest.substring(1)));
+      return;
+    }
+
+    Gerrit.display(token, new NotFoundScreen());
   }
 
   private static void change(final String token) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
index 74a2678..13bba12 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
@@ -25,6 +25,7 @@
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.user.client.PluginSafePopupPanel;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
@@ -94,6 +95,11 @@
     body.add(message.toBlockWidget());
   }
 
+  public ErrorDialog(final Widget w) {
+    this();
+    body.add(w);
+  }
+
   /** Create a dialog box to nicely format an exception. */
   public ErrorDialog(final Throwable what) {
     this();
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 a8315d8..574f58e 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
@@ -104,6 +104,7 @@
   String errorDialogTitle();
   String errorDialogButtons();
   String errorDialogErrorType();
+  String errorDialogText();
   String fileColumnHeader();
   String fileLine();
   String fileLineCONTEXT();
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 9250ca3..d049ff6 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
@@ -89,6 +89,7 @@
   String initialRevision();
   String buttonAddBranch();
   String buttonDeleteBranch();
+  String branchDeletionOpenChanges();
 
   String groupListPrev();
   String groupListNext();
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 389ed2c..7e0edec 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
@@ -68,6 +68,8 @@
 initialRevision = Initial Revision
 buttonAddBranch = Create Branch
 buttonDeleteBranch = Delete
+branchDeletionOpenChanges = The following branches were not deleted \
+because they have open changes:
 
 groupListPrev = Previous group
 groupListNext = Next group
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index f3ecbb3..56d9417 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -16,16 +16,19 @@
 
 import com.google.gerrit.client.ConfirmationCallback;
 import com.google.gerrit.client.ConfirmationDialog;
+import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.GitwebLink;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.HintTextBox;
 import com.google.gerrit.common.data.ListBranchesResult;
 import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.common.errors.InvalidRevisionException;
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
@@ -41,6 +44,7 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
 
@@ -260,24 +264,52 @@
               b.toSafeHtml(), new ConfirmationCallback() {
         @Override
         public void onOk() {
-          Util.PROJECT_SVC.deleteBranch(getProjectKey(), ids,
-              new GerritCallback<Set<Branch.NameKey>>() {
-                public void onSuccess(final Set<Branch.NameKey> deleted) {
-                  for (int row = 1; row < table.getRowCount();) {
-                    final Branch k = getRowItem(row);
-                    if (k != null && deleted.contains(k.getNameKey())) {
-                      table.removeRow(row);
-                    } else {
-                      row++;
-                    }
-                  }
-                }
-              });
+          deleteBranches(ids);
         }
       });
       confirmationDialog.center();
     }
 
+    private void deleteBranches(final Set<Branch.NameKey> branchIds) {
+      Util.PROJECT_SVC.deleteBranch(getProjectKey(), branchIds,
+          new GerritCallback<Set<Branch.NameKey>>() {
+            public void onSuccess(final Set<Branch.NameKey> deleted) {
+              if (!deleted.isEmpty()) {
+                for (int row = 1; row < table.getRowCount();) {
+                  final Branch k = getRowItem(row);
+                  if (k != null && deleted.contains(k.getNameKey())) {
+                    table.removeRow(row);
+                  } else {
+                    row++;
+                  }
+                }
+              }
+
+              branchIds.removeAll(deleted);
+              if (!branchIds.isEmpty()) {
+                final VerticalPanel p = new VerticalPanel();
+                final ErrorDialog errorDialog = new ErrorDialog(p);
+                final Label l = new Label(Util.C.branchDeletionOpenChanges());
+                l.setStyleName(Gerrit.RESOURCES.css().errorDialogText());
+                p.add(l);
+                for (final Branch.NameKey branch : branchIds) {
+                  final BranchLink link =
+                      new BranchLink(branch.getParentKey(), Change.Status.NEW,
+                          branch.get(), null) {
+                    @Override
+                    public void go() {
+                      errorDialog.hide();
+                      super.go();
+                    };
+                  };
+                  p.add(link);
+                }
+                errorDialog.center();
+              }
+            }
+          });
+    }
+
     void display(final List<Branch> result) {
       canDelete = false;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
index 1d881b6..be892a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -14,39 +14,46 @@
 
 package com.google.gerrit.client.changes;
 
-import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeTable.ApprovalViewType;
+import com.google.gerrit.client.NotFoundScreen;
+import com.google.gerrit.client.rpc.NativeList;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AccountDashboardInfo;
-import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
 
+import java.util.Collections;
+import java.util.Comparator;
 
 public class AccountDashboardScreen extends Screen implements ChangeListScreen {
   private final Account.Id ownerId;
-  private ChangeTable table;
-  private ChangeTable.Section byOwner;
-  private ChangeTable.Section forReview;
-  private ChangeTable.Section closed;
+  private final boolean mine;
+  private ChangeTable2 table;
+  private ChangeTable2.Section outgoing;
+  private ChangeTable2.Section incoming;
+  private ChangeTable2.Section closed;
 
   public AccountDashboardScreen(final Account.Id id) {
     ownerId = id;
+    mine = Gerrit.isSignedIn() && ownerId.equals(Gerrit.getUserAccount().getId());
   }
 
   @Override
   protected void onInitUI() {
     super.onInitUI();
-    table = new ChangeTable(true);
+    table = new ChangeTable2();
     table.addStyleName(Gerrit.RESOURCES.css().accountDashboard());
-    byOwner = new ChangeTable.Section("", ApprovalViewType.STRONGEST, null);
-    forReview = new ChangeTable.Section("", ApprovalViewType.USER, ownerId);
-    closed = new ChangeTable.Section("", ApprovalViewType.STRONGEST, null);
 
-    table.addSection(byOwner);
-    table.addSection(forReview);
+    outgoing = new ChangeTable2.Section();
+    incoming = new ChangeTable2.Section();
+    closed = new ChangeTable2.Section();
+
+    outgoing.setTitleText(Util.C.outgoingReviews());
+    incoming.setTitleText(Util.C.incomingReviews());
+    closed.setTitleText(Util.C.recentlyClosed());
+
+    table.addSection(outgoing);
+    table.addSection(incoming);
     table.addSection(closed);
     add(table);
     table.setSavePointerId(PageLinks.toAccountDashboard(ownerId));
@@ -55,13 +62,17 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.LIST_SVC.forAccount(ownerId,
-        new ScreenLoadCallback<AccountDashboardInfo>(this) {
+    String who = mine ? "self" : ownerId.toString();
+    ChangeList.query(
+        new ScreenLoadCallback<NativeList<ChangeList>>(this) {
           @Override
-          protected void preDisplay(final AccountDashboardInfo r) {
-            display(r);
+          protected void preDisplay(NativeList<ChangeList> result) {
+            display(result);
           }
-        });
+        },
+        "is:open owner:" + who,
+        "is:open reviewer:" + who + " -owner:" + who,
+        "is:closed owner:" + who + " -age:1w limit:10");
   }
 
   @Override
@@ -70,20 +81,80 @@
     table.setRegisterKeys(true);
   }
 
-  private void display(final AccountDashboardInfo r) {
-    table.setAccountInfoCache(r.getAccounts());
+  private void display(NativeList<ChangeList> result) {
+    if (!mine && !hasChanges(result)) {
+      // When no results are returned and the data is not for the
+      // current user, the target user is presumed to not exist.
+      Gerrit.display(getToken(), new NotFoundScreen());
+      return;
+    }
 
-    final AccountInfo o = r.getAccounts().get(r.getOwner());
-    final String name = FormatUtil.name(o);
-    setWindowTitle(name);
-    setPageTitle(Util.M.accountDashboardTitle(name));
-    byOwner.setTitleText(Util.M.changesStartedBy(name));
-    forReview.setTitleText(Util.M.changesReviewableBy(name));
-    closed.setTitleText(Util.C.changesRecentlyClosed());
+    ChangeList out = result.get(0);
+    ChangeList in = result.get(1);
+    ChangeList done = result.get(2);
 
-    byOwner.display(r.getByOwner());
-    forReview.display(r.getForReview());
-    closed.display(r.getClosed());
+    if (mine) {
+      setWindowTitle(Util.C.myDashboardTitle());
+      setPageTitle(Util.C.myDashboardTitle());
+    } else {
+      // The server doesn't tell us who the dashboard is for. Try to guess
+      // by looking at a change started by the owner and extract the name.
+      String name = guessName(out);
+      if (name == null) {
+        name = guessName(done);
+      }
+      if (name != null) {
+        setWindowTitle(name);
+        setPageTitle(Util.M.accountDashboardTitle(name));
+      } else {
+        setWindowTitle(Util.C.unknownDashboardTitle());
+        setWindowTitle(Util.C.unknownDashboardTitle());
+      }
+    }
+
+    Collections.sort(out.asList(), compare());
+    Collections.sort(in.asList(), compare());
+
+    table.updateColumnsForLabels(out, in, done);
+    outgoing.display(out);
+    incoming.display(in);
+    closed.display(done);
     table.finishDisplay();
   }
+
+  private Comparator<ChangeInfo> compare() {
+    return new Comparator<ChangeInfo>() {
+      @Override
+      public int compare(ChangeInfo a, ChangeInfo b) {
+        int cmp = a.project().compareTo(b.project());
+        if (cmp != 0) return cmp;
+        cmp = a.branch().compareTo(b.branch());
+        if (cmp != 0) return cmp;
+
+        String at = a.topic() != null ? a.topic() : "";
+        String bt = b.topic() != null ? b.topic() : "";
+        cmp = at.compareTo(bt);
+        if (cmp != 0) return cmp;
+        return a._number() - b._number();
+      }
+    };
+  }
+
+  private boolean hasChanges(NativeList<ChangeList> result) {
+    for (ChangeList list : result.asList()) {
+      if (!list.isEmpty()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static String guessName(ChangeList list) {
+    for (ChangeInfo change : list.asList()) {
+      if (change.owner() != null && change.owner().name() != null) {
+        return change.owner().name();
+      }
+    }
+    return null;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
index 27f76f6..5a9da1a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
@@ -38,7 +38,6 @@
   private Change.Id changeId;
   private ChangeDetailCache detail;
   private ListenableValue<ChangeInfo> info;
-  private StarCache starred;
 
   protected ChangeCache(Change.Id chg) {
     changeId = chg;
@@ -61,11 +60,4 @@
     }
     return info;
   }
-
-  public StarCache getStarCache() {
-    if (starred == null) {
-      starred = new StarCache(changeId);
-    }
-    return starred;
-  }
 }
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 3372096..d42992f 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
@@ -23,7 +23,11 @@
   String statusLongAbandoned();
   String statusLongDraft();
 
-  String changesRecentlyClosed();
+  String myDashboardTitle();
+  String unknownDashboardTitle();
+  String incomingReviews();
+  String outgoingReviews();
+  String recentlyClosed();
 
   String starredHeading();
   String watchedHeading();
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 ad70674..8ceb74c 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
@@ -7,7 +7,11 @@
 starredHeading = Starred Changes
 watchedHeading = Open Changes of Watched Projects
 draftsHeading = Changes with unpublished drafts
-changesRecentlyClosed = Recently closed
+myDashboardTitle = My Reviews
+unknownDashboardTitle = Code Review Dashboard
+incomingReviews = Incoming reviews
+outgoingReviews = Outgoing reviews
+recentlyClosed = Recently closed
 allOpenChanges = All open changes
 allAbandonedChanges = All abandoned changes
 allMergedChanges = All merged changes
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
index bb28e11..9c19d50 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
@@ -65,6 +65,7 @@
   public static void setChangeDetail(ChangeDetail detail) {
     Change.Id chgId = detail.getChange().getId();
     ChangeCache.get(chgId).getChangeDetailCache().set(detail);
+    StarredChanges.fireChangeStarEvent(chgId, detail.isStarred());
   }
 
   private final Change.Id changeId;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
new file mode 100644
index 0000000..adacade
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
@@ -0,0 +1,106 @@
+// 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.client.changes;
+
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
+
+import java.sql.Timestamp;
+import java.util.Set;
+
+public class ChangeInfo extends JavaScriptObject {
+  public final Project.NameKey project_name_key() {
+    return new Project.NameKey(project());
+  }
+
+  public final Change.Id legacy_id() {
+    return new Change.Id(_number());
+  }
+
+  public final Timestamp updated() {
+    return JavaSqlTimestamp_JsonSerializer.parseTimestamp(updatedRaw());
+  }
+
+  public final String id_abbreviated() {
+    return new Change.Key(id()).abbreviate();
+  }
+
+  public final Change.Status status() {
+    return Change.Status.valueOf(statusRaw());
+  }
+
+  public final Set<String> labels() {
+    return Natives.keys(labels0());
+  }
+
+  public final native String project() /*-{ return this.project; }-*/;
+  public final native String branch() /*-{ return this.branch; }-*/;
+  public final native String topic() /*-{ return this.topic; }-*/;
+  public final native String id() /*-{ return this.id; }-*/;
+  private final native String statusRaw() /*-{ return this.status; }-*/;
+  public final native String subject() /*-{ return this.subject; }-*/;
+  public final native AccountInfo owner() /*-{ return this.owner; }-*/;
+  private final native String updatedRaw() /*-{ return this.updated; }-*/;
+  public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
+  public final native String _sortkey() /*-{ return this._sortkey; }-*/;
+  private final native JavaScriptObject labels0() /*-{ return this.labels; }-*/;
+  public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
+  final native int _number() /*-{ return this._number; }-*/;
+  final native boolean _more_changes()
+  /*-{ return this._more_changes ? true : false; }-*/;
+
+  protected ChangeInfo() {
+  }
+
+  public static class AccountInfo extends JavaScriptObject {
+    public final native String name() /*-{ return this.name; }-*/;
+
+    protected AccountInfo() {
+    }
+  }
+
+  public static class LabelInfo extends JavaScriptObject {
+    public final SubmitRecord.Label.Status status() {
+      if (approved() != null) {
+        return SubmitRecord.Label.Status.OK;
+      } else if (rejected() != null) {
+        return SubmitRecord.Label.Status.REJECT;
+      } else {
+        return SubmitRecord.Label.Status.NEED;
+      }
+    }
+
+    public final native String name() /*-{ return this._name; }-*/;
+    public final native AccountInfo approved() /*-{ return this.approved; }-*/;
+    public final native AccountInfo rejected() /*-{ return this.rejected; }-*/;
+
+    public final native AccountInfo recommended() /*-{ return this.recommended; }-*/;
+    public final native AccountInfo disliked() /*-{ return this.disliked; }-*/;
+    final native short _value()
+    /*-{
+      if (this.value) return this.value;
+      if (this.recommended) return 1;
+      if (this.disliked) return -1;
+      return 0;
+    }-*/;
+
+    protected LabelInfo() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
new file mode 100644
index 0000000..debe145
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
@@ -0,0 +1,74 @@
+// 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.client.changes;
+
+import com.google.gerrit.client.rpc.NativeList;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtorm.client.KeyUtil;
+
+/** List of changes available from {@code /changes/}. */
+public class ChangeList extends NativeList<ChangeInfo> {
+  private static final String URI = "/changes/";
+
+  /** Run 2 or more queries in a single remote invocation. */
+  public static void query(
+      AsyncCallback<NativeList<ChangeList>> callback, String... queries) {
+    assert queries.length >= 2; // At least 2 is required for correct result.
+    RestApi call = new RestApi(URI);
+    for (String q : queries) {
+      call.addParameterRaw("q", KeyUtil.encode(q));
+    }
+    call.send(callback);
+  }
+
+  public static void prev(String query,
+      int limit, String sortkey,
+      AsyncCallback<ChangeList> callback) {
+    RestApi call = newQuery(query);
+    if (limit > 0) {
+      call.addParameter("n", limit);
+    }
+    if (!PagedSingleListScreen.MIN_SORTKEY.equals(sortkey)) {
+      call.addParameter("P", sortkey);
+    }
+    call.send(callback);
+  }
+
+  public static void next(String query,
+      int limit, String sortkey,
+      AsyncCallback<ChangeList> callback) {
+    RestApi call = newQuery(query);
+    if (limit > 0) {
+      call.addParameter("n", limit);
+    }
+    if (!PagedSingleListScreen.MAX_SORTKEY.equals(sortkey)) {
+      call.addParameter("N", sortkey);
+    }
+    call.send(callback);
+  }
+
+  private static RestApi newQuery(String query) {
+    RestApi call = new RestApi(URI);
+    // The server default is ?q=status:open so don't repeat it.
+    if (!"status:open".equals(query) && !"is:open".equals(query)) {
+      call.addParameterRaw("q", KeyUtil.encode(query));
+    }
+    return call;
+  }
+
+  protected ChangeList() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index 41dc6c1..4bd0828 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -18,8 +18,6 @@
 
 public interface ChangeMessages extends Messages {
   String accountDashboardTitle(String fullName);
-  String changesStartedBy(String fullName);
-  String changesReviewableBy(String fullName);
   String changesOpenInProject(String string);
   String changesMergedInProject(String string);
   String changesAbandonedInProject(String string);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index 40088a1..2449613 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -1,6 +1,4 @@
 accountDashboardTitle = Code Review Dashboard for {0}
-changesStartedBy = Started by {0}
-changesReviewableBy = Review Requests for {0}
 changesOpenInProject = Open Changes In {0}
 changesMergedInProject = Merged Changes In {0}
 changesAbandonedInProject = Abandoned Changes In {0}
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 b054175..15c1150 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
@@ -28,9 +28,9 @@
 import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
@@ -42,7 +42,6 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.ListBox;
@@ -60,9 +59,7 @@
   private final Change.Id changeId;
   private final PatchSet.Id openPatchSetId;
   private ChangeDetailCache detailCache;
-  private StarCache starred;
 
-  private Image starChange;
   private ChangeDescriptionBlock descriptionBlock;
   private ApprovalTable approvals;
 
@@ -155,8 +152,6 @@
     detailCache = cache.getChangeDetailCache();
     detailCache.addValueChangeHandler(this);
 
-    starred = cache.getStarCache();
-
     addStyleName(Gerrit.RESOURCES.css().changeScreen());
 
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
@@ -165,13 +160,13 @@
     keysNavigation.add(new ExpandCollapseDependencySectionKeyCommand(0, 'd', Util.C.expandCollapseDependencies()));
 
     if (Gerrit.isSignedIn()) {
-      keysAction.add(starred.new KeyCommand(0, 's', Util.C.changeTableStar()));
+      StarredChanges.Icon star = StarredChanges.createIcon(changeId, false);
+      star.setStyleName(Gerrit.RESOURCES.css().changeScreenStarIcon());
+      setTitleWest(star);
+
+      keysAction.add(StarredChanges.newKeyCommand(star));
       keysAction.add(new PublishCommentsKeyCommand(0, 'r', Util.C
           .keyPublishComments()));
-
-      starChange = starred.createStar();
-      starChange.setStyleName(Gerrit.RESOURCES.css().changeScreenStarIcon());
-      setTitleWest(starChange);
     }
 
     descriptionBlock = new ChangeDescriptionBlock();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 111e507..19a770e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -145,7 +145,7 @@
   protected void onStarClick(final int row) {
     final ChangeInfo c = getRowItem(row);
     if (c != null && Gerrit.isSignedIn()) {
-       ChangeCache.get(c.getId()).getStarCache().toggleStar();
+      ((StarredChanges.Icon) table.getWidget(row, C_STAR)).toggleStar();
     }
   }
 
@@ -198,7 +198,7 @@
     final String idstr = c.getKey().abbreviate();
     table.setWidget(row, C_ARROW, null);
     if (Gerrit.isSignedIn()) {
-      table.setWidget(row, C_STAR, cache.getStarCache().createStar());
+      table.setWidget(row, C_STAR, StarredChanges.createIcon(c.getId(), c.isStarred()));
     }
     table.setWidget(row, C_ID, new TableChangeLink(idstr, c));
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
new file mode 100644
index 0000000..1372aa2
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
@@ -0,0 +1,403 @@
+// Copyright (C) 2008 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import static com.google.gerrit.client.FormatUtil.shortFormat;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.ui.BranchLink;
+import com.google.gerrit.client.ui.ChangeLink;
+import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
+import com.google.gerrit.client.ui.ProjectLink;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLTable.Cell;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.InlineLabel;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ChangeTable2 extends NavigationTable<ChangeInfo> {
+  private static final int C_STAR = 1;
+  private static final int C_ID = 2;
+  private static final int C_SUBJECT = 3;
+  private static final int C_OWNER = 4;
+  private static final int C_PROJECT = 5;
+  private static final int C_BRANCH = 6;
+  private static final int C_LAST_UPDATE = 7;
+  private static final int BASE_COLUMNS = 8;
+
+  private final List<Section> sections;
+  private int columns;
+  private List<String> labelNames;
+
+  public ChangeTable2() {
+    columns = BASE_COLUMNS;
+    labelNames = Collections.emptyList();
+
+    keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.changeTablePrev()));
+    keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.changeTableNext()));
+    keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.changeTableOpen()));
+    keysNavigation.add(
+        new OpenKeyCommand(0, KeyCodes.KEY_ENTER, Util.C.changeTableOpen()));
+
+    if (Gerrit.isSignedIn()) {
+      keysAction.add(new StarKeyCommand(0, 's', Util.C.changeTableStar()));
+    }
+
+    sections = new ArrayList<Section>();
+    table.setText(0, C_STAR, "");
+    table.setText(0, C_ID, Util.C.changeTableColumnID());
+    table.setText(0, C_SUBJECT, Util.C.changeTableColumnSubject());
+    table.setText(0, C_OWNER, Util.C.changeTableColumnOwner());
+    table.setText(0, C_PROJECT, Util.C.changeTableColumnProject());
+    table.setText(0, C_BRANCH, Util.C.changeTableColumnBranch());
+    table.setText(0, C_LAST_UPDATE, Util.C.changeTableColumnLastUpdate());
+
+    final FlexCellFormatter fmt = table.getFlexCellFormatter();
+    fmt.addStyleName(0, C_STAR, Gerrit.RESOURCES.css().iconHeader());
+    fmt.addStyleName(0, C_ID, Gerrit.RESOURCES.css().cID());
+    for (int i = C_ID; i < columns; i++) {
+      fmt.addStyleName(0, i, Gerrit.RESOURCES.css().dataHeader());
+    }
+
+    table.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        final Cell cell = table.getCellForEvent(event);
+        if (cell == null) {
+          return;
+        }
+        if (cell.getCellIndex() == C_STAR) {
+          // Don't do anything (handled by star itself).
+        } else if (cell.getCellIndex() == C_OWNER) {
+          // Don't do anything.
+        } else if (getRowItem(cell.getRowIndex()) != null) {
+          movePointerTo(cell.getRowIndex());
+        }
+      }
+    });
+  }
+
+  @Override
+  protected Object getRowItemKey(final ChangeInfo item) {
+    return item.legacy_id();
+  }
+
+  @Override
+  protected void onOpenRow(final int row) {
+    final ChangeInfo c = getRowItem(row);
+    final Change.Id id = c.legacy_id();
+    Gerrit.display(PageLinks.toChange(id), new ChangeScreen(id));
+  }
+
+  private void insertNoneRow(final int row) {
+    insertRow(row);
+    table.setText(row, 0, Util.C.changeTableNone());
+    final FlexCellFormatter fmt = table.getFlexCellFormatter();
+    fmt.setColSpan(row, 0, columns);
+    fmt.setStyleName(row, 0, Gerrit.RESOURCES.css().emptySection());
+  }
+
+  private void insertChangeRow(final int row) {
+    insertRow(row);
+    applyDataRowStyle(row);
+  }
+
+  @Override
+  protected void applyDataRowStyle(final int row) {
+    super.applyDataRowStyle(row);
+    final CellFormatter fmt = table.getCellFormatter();
+    fmt.addStyleName(row, C_STAR, Gerrit.RESOURCES.css().iconCell());
+    for (int i = C_ID; i < columns; i++) {
+      fmt.addStyleName(row, i, Gerrit.RESOURCES.css().dataCell());
+    }
+    fmt.addStyleName(row, C_ID, Gerrit.RESOURCES.css().cID());
+    fmt.addStyleName(row, C_SUBJECT, Gerrit.RESOURCES.css().cSUBJECT());
+    fmt.addStyleName(row, C_PROJECT, Gerrit.RESOURCES.css().cPROJECT());
+    fmt.addStyleName(row, C_BRANCH, Gerrit.RESOURCES.css().cPROJECT());
+    fmt.addStyleName(row, C_LAST_UPDATE, Gerrit.RESOURCES.css().cLastUpdate());
+    for (int i = BASE_COLUMNS; i < columns; i++) {
+      fmt.addStyleName(row, i, Gerrit.RESOURCES.css().cAPPROVAL());
+    }
+  }
+
+  public void updateColumnsForLabels(ChangeList... lists) {
+    labelNames = new ArrayList<String>();
+    for (ChangeList list : lists) {
+      for (int i = 0; i < list.size(); i++) {
+        for (String name : list.get(i).labels()) {
+          if (!labelNames.contains(name)) {
+            labelNames.add(name);
+          }
+        }
+      }
+    }
+    Collections.sort(labelNames);
+
+    if (BASE_COLUMNS + labelNames.size() < columns) {
+      int n = columns - (BASE_COLUMNS + labelNames.size());
+      for (int row = 0; row < table.getRowCount(); row++) {
+        table.removeCells(row, columns, n);
+      }
+    }
+    columns = BASE_COLUMNS + labelNames.size();
+
+    FlexCellFormatter fmt = table.getFlexCellFormatter();
+    for (int i = 0; i < labelNames.size(); i++) {
+      String name = labelNames.get(i);
+      int col = BASE_COLUMNS + i;
+
+      StringBuilder abbrev = new StringBuilder();
+      for (String t : name.split("-")) {
+        abbrev.append(t.substring(0, 1).toUpperCase());
+      }
+      table.setText(0, col, abbrev.toString());
+      table.getCellFormatter().getElement(0, col).setTitle(name);
+      fmt.addStyleName(0, col, Gerrit.RESOURCES.css().dataHeader());
+    }
+
+    for (Section s : sections) {
+      if (s.titleRow >= 0) {
+        fmt.setColSpan(s.titleRow, 0, columns);
+      }
+    }
+  }
+
+  private void populateChangeRow(final int row, final ChangeInfo c) {
+    if (Gerrit.isSignedIn()) {
+      table.setWidget(row, C_STAR, StarredChanges.createIcon(
+          c.legacy_id(),
+          c.starred()));
+    }
+    table.setWidget(row, C_ID, new TableChangeLink(c.id_abbreviated(), c));
+
+    String subject = c.subject();
+    if (subject.length() > 80) {
+      subject = subject.substring(0, 80);
+    }
+    Change.Status status = c.status();
+    if (status != Change.Status.NEW) {
+      subject += " (" + Util.toLongString(status) + ")";
+    }
+    table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
+
+    String owner = "";
+    if (c.owner() != null && c.owner().name() != null) {
+      owner = c.owner().name();
+    }
+    table.setText(row, C_OWNER, owner);
+
+    table.setWidget(
+        row, C_PROJECT, new ProjectLink(c.project_name_key(), c.status()));
+    table.setWidget(row, C_BRANCH, new BranchLink(c.project_name_key(), c
+        .status(), c.branch(), c.topic()));
+    table.setText(row, C_LAST_UPDATE, shortFormat(c.updated()));
+
+    boolean displayName = Gerrit.isSignedIn() && Gerrit.getUserAccount()
+        .getGeneralPreferences().isShowUsernameInReviewCategory();
+
+    CellFormatter fmt = table.getCellFormatter();
+    for (int idx = 0; idx < labelNames.size(); idx++) {
+      String name = labelNames.get(idx);
+      int col = BASE_COLUMNS + idx;
+
+      LabelInfo label = c.label(name);
+      if (label == null) {
+        table.clearCell(row, col);
+        continue;
+      }
+
+      String user;
+      if (label.rejected() != null) {
+        user = label.rejected().name();
+        if (displayName && user != null) {
+          FlowPanel panel = new FlowPanel();
+          panel.add(new Image(Gerrit.RESOURCES.redNot()));
+          panel.add(new InlineLabel(user));
+          table.setWidget(row, col, panel);
+        } else {
+          table.setWidget(row, col, new Image(Gerrit.RESOURCES.redNot()));
+        }
+      } else if (label.approved() != null) {
+        user = label.approved().name();
+        if (displayName && user != null) {
+          FlowPanel panel = new FlowPanel();
+          panel.add(new Image(Gerrit.RESOURCES.greenCheck()));
+          panel.add(new InlineLabel(user));
+          table.setWidget(row, col, panel);
+        } else {
+          table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
+        }
+      } else if (label.disliked() != null) {
+        user = label.disliked().name();
+        String vstr = String.valueOf(label._value());
+        if (displayName && user != null) {
+          vstr = vstr + " " + user;
+        }
+        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().negscore());
+        table.setText(row, col, vstr);
+      } else if (label.recommended() != null) {
+        user = label.recommended().name();
+        String vstr = "+" + label._value();
+        if (displayName && user != null) {
+          vstr = vstr + " " + user;
+        }
+        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().posscore());
+        table.setText(row, col, vstr);
+      } else {
+        table.clearCell(row, col);
+        continue;
+      }
+      fmt.addStyleName(row, col, Gerrit.RESOURCES.css().singleLine());
+
+      if (!displayName && user != null) {
+        // Some web browsers ignore the embedded newline; some like it;
+        // so we include a space before the newline to accommodate both.
+        fmt.getElement(row, col).setTitle(name + " \nby " + user);
+      }
+    }
+
+    // TODO(sop): Highlight changes I haven't reviewed on my dashboard.
+    // final Element tr = DOM.getParent(fmt.getElement(row, 0));
+    // UIObject.setStyleName(tr, Gerrit.RESOURCES.css().needsReview(),
+    // !haveReview && highlightUnreviewed);
+
+    setRowItem(row, c);
+  }
+
+  public void addSection(final Section s) {
+    assert s.parent == null;
+
+    if (s.titleText != null) {
+      s.titleRow = table.getRowCount();
+      table.setText(s.titleRow, 0, s.titleText);
+      final FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.setColSpan(s.titleRow, 0, columns);
+      fmt.addStyleName(s.titleRow, 0, Gerrit.RESOURCES.css().sectionHeader());
+    } else {
+      s.titleRow = -1;
+    }
+
+    s.parent = this;
+    s.dataBegin = table.getRowCount();
+    insertNoneRow(s.dataBegin);
+    sections.add(s);
+  }
+
+  private int insertRow(final int beforeRow) {
+    for (final Section s : sections) {
+      if (beforeRow <= s.titleRow) {
+        s.titleRow++;
+      }
+      if (beforeRow < s.dataBegin) {
+        s.dataBegin++;
+      }
+    }
+    return table.insertRow(beforeRow);
+  }
+
+  private void removeRow(final int row) {
+    for (final Section s : sections) {
+      if (row < s.titleRow) {
+        s.titleRow--;
+      }
+      if (row < s.dataBegin) {
+        s.dataBegin--;
+      }
+    }
+    table.removeRow(row);
+  }
+
+  public class StarKeyCommand extends NeedsSignInKeyCommand {
+    public StarKeyCommand(int mask, char key, String help) {
+      super(mask, key, help);
+    }
+
+    @Override
+    public void onKeyPress(final KeyPressEvent event) {
+      int row = getCurrentRow();
+      ChangeInfo c = getRowItem(row);
+      if (c != null && Gerrit.isSignedIn()) {
+        ((StarredChanges.Icon) table.getWidget(row, C_STAR)).toggleStar();
+      }
+    }
+  }
+
+  private final class TableChangeLink extends ChangeLink {
+    private TableChangeLink(final String text, final ChangeInfo c) {
+      super(text, c.legacy_id());
+    }
+
+    @Override
+    public void go() {
+      movePointerTo(cid);
+      super.go();
+    }
+  }
+
+  public static class Section {
+    ChangeTable2 parent;
+    String titleText;
+    int titleRow = -1;
+    int dataBegin;
+    int rows;
+
+    public void setTitleText(final String text) {
+      titleText = text;
+      if (titleRow >= 0) {
+        parent.table.setText(titleRow, 0, titleText);
+      }
+    }
+
+    public void display(ChangeList changeList) {
+      final int sz = changeList != null ? changeList.size() : 0;
+      final boolean hadData = rows > 0;
+
+      if (hadData) {
+        while (sz < rows) {
+          parent.removeRow(dataBegin);
+          rows--;
+        }
+      } else {
+        parent.removeRow(dataBegin);
+      }
+
+      if (sz == 0) {
+        parent.insertNoneRow(dataBegin);
+        return;
+      }
+
+      while (rows < sz) {
+        parent.insertChangeRow(dataBegin + rows);
+        rows++;
+      }
+      for (int i = 0; i < sz; i++) {
+        parent.populateChangeRow(dataBegin + i, changeList.get(i));
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
new file mode 100644
index 0000000..c9f1dc6
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
@@ -0,0 +1,112 @@
+// 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.client.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.NativeList;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.Screen;
+import com.google.gwt.http.client.URL;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CustomDashboardScreen extends Screen implements ChangeListScreen {
+  private String title;
+  private List<String> titles;
+  private List<String> queries;
+  private ChangeTable2 table;
+  private List<ChangeTable2.Section> sections;
+
+  public CustomDashboardScreen(String params) {
+    titles = new ArrayList<String>();
+    queries = new ArrayList<String>();
+    for (String kvPair : params.split("[,;&]")) {
+      String[] kv = kvPair.split("=", 2);
+      if (kv.length != 2 || kv[0].isEmpty()) {
+        continue;
+      }
+
+      if ("title".equals(kv[0])) {
+        title = URL.decodeQueryString(kv[1]);
+      } else {
+        titles.add(URL.decodeQueryString(kv[0]));
+        queries.add(URL.decodeQueryString(kv[1]));
+      }
+    }
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+
+    if (title != null) {
+      setWindowTitle(title);
+      setPageTitle(title);
+    }
+
+    table = new ChangeTable2();
+    table.addStyleName(Gerrit.RESOURCES.css().accountDashboard());
+
+    sections = new ArrayList<ChangeTable2.Section>();
+    for (String title : titles) {
+      ChangeTable2.Section s = new ChangeTable2.Section();
+      s.setTitleText(title);
+      table.addSection(s);
+      sections.add(s);
+    }
+    add(table);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+
+    if (queries.isEmpty()) {
+      display();
+    } else if (queries.size() == 1) {
+      ChangeList.next(queries.get(0),
+          0, PagedSingleListScreen.MAX_SORTKEY,
+          new ScreenLoadCallback<ChangeList>(this) {
+            @Override
+            protected void preDisplay(ChangeList result) {
+              table.updateColumnsForLabels(result);
+              sections.get(0).display(result);
+              table.finishDisplay();
+            }
+        });
+    } else {
+      ChangeList.query(
+          new ScreenLoadCallback<NativeList<ChangeList>>(this) {
+            @Override
+            protected void preDisplay(NativeList<ChangeList> result) {
+              table.updateColumnsForLabels(
+                  result.asList().toArray(new ChangeList[result.size()]));
+              for (int i = 0; i < result.size(); i++) {
+                sections.get(i).display(result.get(i));
+              }
+              table.finishDisplay();
+            }
+          },
+          queries.toArray(new String[queries.size()]));
+    }
+  }
+
+  @Override
+  public void registerKeys() {
+    super.registerKeys();
+    table.setRegisterKeys(true);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
index 72300b2..23ce178 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
@@ -15,12 +15,9 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeTable.ApprovalViewType;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.SingleListChangeInfo;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.user.client.History;
@@ -28,19 +25,16 @@
 import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 
-import java.util.List;
-
-
 public abstract class PagedSingleListScreen extends Screen {
   protected static final String MIN_SORTKEY = "";
   protected static final String MAX_SORTKEY = "z";
 
   protected final int pageSize;
-  private ChangeTable table;
-  private ChangeTable.Section section;
+  private ChangeTable2 table;
+  private ChangeTable2.Section section;
   protected Hyperlink prev;
   protected Hyperlink next;
-  protected List<ChangeInfo> changes;
+  protected ChangeList changes;
 
   protected final String anchorPrefix;
   protected boolean useLoadPrev;
@@ -71,7 +65,7 @@
     next = new Hyperlink(Util.C.pagedChangeListNext(), true, "");
     next.setVisible(false);
 
-    table = new ChangeTable(true) {
+    table = new ChangeTable2() {
       {
         keysNavigation.add(new DoLinkCommand(0, 'p', Util.C
             .changeTablePagePrev(), prev));
@@ -79,8 +73,7 @@
             .changeTablePageNext(), next));
       }
     };
-    section = new ChangeTable.Section(null, ApprovalViewType.STRONGEST, null);
-
+    section = new ChangeTable2.Section();
     table.addSection(section);
     table.setSavePointerId(anchorPrefix);
     add(table);
@@ -112,36 +105,34 @@
 
   protected abstract void loadNext();
 
-  protected AsyncCallback<SingleListChangeInfo> loadCallback() {
-    return new ScreenLoadCallback<SingleListChangeInfo>(this) {
+  protected AsyncCallback<ChangeList> loadCallback() {
+    return new ScreenLoadCallback<ChangeList>(this) {
       @Override
-      protected void preDisplay(final SingleListChangeInfo result) {
+      protected void preDisplay(ChangeList result) {
         display(result);
       }
     };
   }
 
-  protected void display(final SingleListChangeInfo result) {
-    changes = result.getChanges();
-
+  protected void display(final ChangeList result) {
+    changes = result;
     if (!changes.isEmpty()) {
       final ChangeInfo f = changes.get(0);
       final ChangeInfo l = changes.get(changes.size() - 1);
 
-      prev.setTargetHistoryToken(anchorPrefix + ",p," + f.getSortKey());
-      next.setTargetHistoryToken(anchorPrefix + ",n," + l.getSortKey());
+      prev.setTargetHistoryToken(anchorPrefix + ",p," + f._sortkey());
+      next.setTargetHistoryToken(anchorPrefix + ",n," + l._sortkey());
 
       if (useLoadPrev) {
-        prev.setVisible(!result.isAtEnd());
+        prev.setVisible(f._more_changes());
         next.setVisible(!MIN_SORTKEY.equals(pos));
       } else {
         prev.setVisible(!MAX_SORTKEY.equals(pos));
-        next.setVisible(!result.isAtEnd());
+        next.setVisible(l._more_changes());
       }
     }
-
-    table.setAccountInfoCache(result.getAccounts());
-    section.display(result.getChanges());
+    table.updateColumnsForLabels(result);
+    section.display(result);
     table.finishDisplay();
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
index cf9c526..b94fcae 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
@@ -17,13 +17,11 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.SingleListChangeInfo;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
 
-
 public class QueryScreen extends PagedSingleListScreen implements
     ChangeListScreen {
   public static QueryScreen forQuery(String query) {
@@ -49,13 +47,15 @@
   }
 
   @Override
-  protected AsyncCallback<SingleListChangeInfo> loadCallback() {
-    return new GerritCallback<SingleListChangeInfo>() {
-      public final void onSuccess(final SingleListChangeInfo result) {
+  protected AsyncCallback<ChangeList> loadCallback() {
+    return new GerritCallback<ChangeList>() {
+      @Override
+      public final void onSuccess(ChangeList result) {
         if (isAttached()) {
-          if (result.getChanges().size() == 1 && isSingleQuery(query)) {
-            final ChangeInfo c = result.getChanges().get(0);
-            Gerrit.display(PageLinks.toChange(c), new ChangeScreen(c));
+          if (result.size() == 1 && isSingleQuery(query)) {
+            ChangeInfo c = result.get(0);
+            Change.Id id = c.legacy_id();
+            Gerrit.display(PageLinks.toChange(id), new ChangeScreen(id));
           } else {
             Gerrit.setQueryString(query);
             display(result);
@@ -68,12 +68,12 @@
 
   @Override
   protected void loadPrev() {
-    Util.LIST_SVC.allQueryPrev(query, pos, pageSize, loadCallback());
+    ChangeList.prev(query, pageSize, pos, loadCallback());
   }
 
   @Override
   protected void loadNext() {
-    Util.LIST_SVC.allQueryNext(query, pos, pageSize, loadCallback());
+    ChangeList.next(query, pageSize, pos, loadCallback());
   }
 
   private static boolean isSingleQuery(String query) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java
deleted file mode 100644
index d7624a6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java
+++ /dev/null
@@ -1,139 +0,0 @@
-// 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.client.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
-import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.ToggleStarRequest;
-import com.google.gerrit.reviewdb.client.Change;
-
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.GwtEvent;
-import com.google.gwt.event.shared.HandlerManager;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.resources.client.ImageResource;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwtjsonrpc.common.VoidResult;
-
-public class StarCache implements HasValueChangeHandlers<Boolean> {
-  public class KeyCommand extends NeedsSignInKeyCommand {
-    public KeyCommand(int mask, char key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      StarCache.this.toggleStar();
-    }
-  }
-
-  ChangeCache cache;
-
-  private HandlerManager manager = new HandlerManager(this);
-
-  public StarCache(final Change.Id chg) {
-    cache = ChangeCache.get(chg);
-  }
-
-  public boolean get() {
-    ChangeDetail detail = cache.getChangeDetailCache().get();
-    if (detail != null) {
-      return detail.isStarred();
-    }
-    ChangeInfo info = cache.getChangeInfoCache().get();
-    if (info != null) {
-      return info.isStarred();
-    }
-    return false;
-  }
-
-  public void set(final boolean s) {
-    if (Gerrit.isSignedIn() && s != get()) {
-      final ToggleStarRequest req = new ToggleStarRequest();
-      req.toggle(cache.getChangeId(), s);
-
-      Util.LIST_SVC.toggleStars(req, new GerritCallback<VoidResult>() {
-        public void onSuccess(final VoidResult result) {
-          setStarred(s);
-          fireEvent(new ValueChangeEvent<Boolean>(s){});
-        }
-      });
-    }
-  }
-
-  private void setStarred(final boolean s) {
-    ChangeDetail detail = cache.getChangeDetailCache().get();
-    if (detail != null) {
-      detail.setStarred(s);
-    }
-    ChangeInfo info = cache.getChangeInfoCache().get();
-    if (info != null) {
-      info.setStarred(s);
-    }
-  }
-
-  public void toggleStar() {
-    set(!get());
-  }
-
-  @SuppressWarnings("unchecked")
-  public Image createStar() {
-    final Image star = new Image(getResource());
-    star.setVisible(Gerrit.isSignedIn());
-
-    star.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        StarCache.this.toggleStar();
-      }
-    });
-
-    @SuppressWarnings("rawtypes")
-    ValueChangeHandler starUpdater = new ValueChangeHandler() {
-        @Override
-        public void onValueChange(ValueChangeEvent event) {
-          star.setResource(StarCache.this.getResource());
-        }
-      };
-
-    cache.getChangeDetailCache().addValueChangeHandler(starUpdater);
-    cache.getChangeInfoCache().addValueChangeHandler(starUpdater);
-
-    this.addValueChangeHandler(starUpdater);
-
-    return star;
-  }
-
-  private ImageResource getResource() {
-    return get() ? Gerrit.RESOURCES.starFilled() : Gerrit.RESOURCES.starOpen();
-  }
-
-  public void fireEvent(GwtEvent<?> event) {
-    manager.fireEvent(event);
-  }
-
-  public HandlerRegistration addValueChangeHandler(
-      ValueChangeHandler<Boolean> handler) {
-    return manager.addHandler(ValueChangeEvent.getType(), handler);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
new file mode 100644
index 0000000..8b5aa1c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
@@ -0,0 +1,216 @@
+// 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.client.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.data.ToggleStarRequest;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.event.shared.SimpleEventBus;
+import com.google.gwt.resources.client.ImageResource;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.web.bindery.event.shared.Event;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+
+/** Supports the star icon displayed on changes and tracking the status. */
+public class StarredChanges {
+  private static final EventBus eventBus = new SimpleEventBus();
+  private static final Event.Type<ChangeStarHandler> TYPE =
+      new Event.Type<ChangeStarHandler>();
+
+  /** Handler that can receive notifications of a change's starred status. */
+  public static interface ChangeStarHandler {
+    public void onChangeStar(ChangeStarEvent event);
+  }
+
+  /** Event fired when a star changes status. The new status is reported. */
+  public static class ChangeStarEvent extends Event<ChangeStarHandler> {
+    private boolean starred;
+
+    public ChangeStarEvent(Change.Id source, boolean starred) {
+      setSource(source);
+      this.starred = starred;
+    }
+
+    public boolean isStarred() {
+      return starred;
+    }
+
+    @Override
+    public Type<ChangeStarHandler> getAssociatedType() {
+      return TYPE;
+    }
+
+    @Override
+    protected void dispatch(ChangeStarHandler handler) {
+      handler.onChangeStar(this);
+    }
+  }
+
+  /**
+   * Create a star icon for the given change, and current status. Returns null
+   * if the user is not signed in and cannot support starred changes.
+   */
+  public static Icon createIcon(Change.Id source, boolean starred) {
+    return Gerrit.isSignedIn() ? new Icon(source, starred) : null;
+  }
+
+  /** Make a key command that toggles the star for a change. */
+  public static KeyCommand newKeyCommand(final Icon icon) {
+    return new KeyCommand(0, 's', Util.C.changeTableStar()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        icon.toggleStar();
+      }
+    };
+  }
+
+  /** Add a handler to listen for starred status to change. */
+  public static HandlerRegistration addHandler(
+      Change.Id source,
+      ChangeStarHandler handler) {
+    return eventBus.addHandlerToSource(TYPE, source, handler);
+  }
+
+  /**
+   * Broadcast the current starred value of a change to UI widgets. This does
+   * not RPC to the server and does not alter the starred status of a change.
+   */
+  public static void fireChangeStarEvent(Change.Id id, boolean starred) {
+    eventBus.fireEventFromSource(
+        new ChangeStarEvent(id, starred),
+        id);
+  }
+
+  /**
+   * Set the starred status of a change. This method broadcasts to all
+   * interested UI widgets and sends an RPC to the server to record the
+   * updated status.
+   */
+  public static void toggleStar(
+      final Change.Id changeId,
+      final boolean newValue) {
+    if (next == null) {
+      next = new ToggleStarRequest();
+    }
+    next.toggle(changeId, newValue);
+    fireChangeStarEvent(changeId, newValue);
+    if (!busy) {
+      start();
+    }
+  }
+
+  private static ToggleStarRequest next;
+  private static boolean busy;
+
+  private static void start() {
+    final ToggleStarRequest req = next;
+    next = null;
+    busy = true;
+
+    Util.LIST_SVC.toggleStars(req, new GerritCallback<VoidResult>() {
+      @Override
+      public void onSuccess(VoidResult result) {
+        if (next != null) {
+          start();
+        } else {
+          busy = false;
+        }
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        rollback(req);
+        if (next != null) {
+          rollback(next);
+          next = null;
+        }
+        busy = false;
+        super.onFailure(caught);
+      }
+    });
+  }
+
+  private static void rollback(ToggleStarRequest req) {
+    if (req.getAddSet() != null) {
+      for (Change.Id id : req.getAddSet()) {
+        fireChangeStarEvent(id, false);
+      }
+    }
+    if (req.getRemoveSet() != null) {
+      for (Change.Id id : req.getRemoveSet()) {
+        fireChangeStarEvent(id, true);
+      }
+    }
+  }
+
+  public static class Icon extends Image
+      implements ChangeStarHandler, ClickHandler {
+    private final Change.Id changeId;
+    private boolean starred;
+    private HandlerRegistration handler;
+
+    Icon(Change.Id changeId, boolean starred) {
+      super(resource(starred));
+      this.changeId = changeId;
+      this.starred = starred;
+      addClickHandler(this);
+    }
+
+    /**
+     * Toggles the state of the star, as if the user clicked on the image. This
+     * will broadcast the new star status to all interested UI widgets, and RPC
+     * to the server to store the changed value.
+     */
+    public void toggleStar() {
+      StarredChanges.toggleStar(changeId, !starred);
+    }
+
+    @Override
+    protected void onLoad() {
+      handler = StarredChanges.addHandler(changeId, this);
+    }
+
+    @Override
+    protected void onUnload() {
+      handler.removeHandler();
+      handler = null;
+    }
+
+    @Override
+    public void onChangeStar(ChangeStarEvent event) {
+      setResource(resource(event.isStarred()));
+      starred = event.isStarred();
+    }
+
+    @Override
+    public void onClick(ClickEvent event) {
+      toggleStar();
+    }
+
+    private static ImageResource resource(boolean starred) {
+      return starred ? Gerrit.RESOURCES.starFilled() : Gerrit.RESOURCES.starOpen();
+    }
+  }
+
+  private StarredChanges() {
+  }
+}
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 87208c6..52c37c3 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
@@ -375,6 +375,18 @@
   width: 100%;
   margin-top: 15px;
 }
+.errorDialogText {
+  font-size: 15px;
+  font-family: verdana;
+}
+.errorDialog a,
+.errorDialog a:visited,
+.errorDialog a:hover {
+  color: white;
+  font-weight: bold;
+  font-size: 15px;
+  font-family: verdana;
+}
 
 
 /** Screen **/
diff --git a/gerrit-httpd/.gitignore b/gerrit-httpd/.gitignore
index 194bedc..5bbeafd 100644
--- a/gerrit-httpd/.gitignore
+++ b/gerrit-httpd/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-httpd.iml
\ No newline at end of file
diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml
index a6374da..ceacb66 100644
--- a/gerrit-httpd/pom.xml
+++ b/gerrit-httpd/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-httpd</artifactId>
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 c299a71..9e69946 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
@@ -24,15 +24,21 @@
 import com.google.gerrit.httpd.raw.StaticServlet;
 import com.google.gerrit.httpd.raw.ToolServlet;
 import com.google.gerrit.httpd.rpc.account.AccountCapabilitiesServlet;
+import com.google.gerrit.httpd.rpc.change.DeprecatedChangeQueryServlet;
+import com.google.gerrit.httpd.rpc.change.ListChangesServlet;
 import com.google.gerrit.httpd.rpc.project.ListProjectsServlet;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwtexpui.server.CacheControlFilter;
+import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.Provider;
 import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.servlet.ServletModule;
 
+import org.eclipse.jgit.lib.Config;
+
 import java.io.IOException;
 
 import javax.servlet.http.HttpServlet;
@@ -40,6 +46,21 @@
 import javax.servlet.http.HttpServletResponse;
 
 class UrlModule extends ServletModule {
+  static class UrlConfig {
+    private final boolean deprecatedQuery;
+
+    @Inject
+    UrlConfig(@GerritServerConfig Config cfg) {
+      deprecatedQuery = cfg.getBoolean("site", "enableDeprecatedQuery", true);
+    }
+  }
+
+  private final UrlConfig cfg;
+
+  UrlModule(UrlConfig cfg) {
+    this.cfg = cfg;
+  }
+
   @Override
   protected void configureServlets() {
     filter("/*").through(Key.get(CacheControlFilter.class));
@@ -50,7 +71,6 @@
     serve("/Gerrit/*").with(legacyGerritScreen());
     serve("/cat/*").with(CatServlet.class);
     serve("/logout").with(HttpLogoutServlet.class);
-    serve("/query").with(ChangeQueryServlet.class);
     serve("/signout").with(HttpLogoutServlet.class);
     serve("/ssh_info").with(SshInfoServlet.class);
     serve("/static/*").with(StaticServlet.class);
@@ -74,7 +94,12 @@
 
     filter("/a/*").through(RequireIdentifiedUserFilter.class);
     serveRegex("^/(?:a/)?accounts/self/capabilities$").with(AccountCapabilitiesServlet.class);
+    serveRegex("^/(?:a/)?changes/$").with(ListChangesServlet.class);
     serveRegex("^/(?:a/)?projects/(.*)?$").with(ListProjectsServlet.class);
+
+    if (cfg.deprecatedQuery) {
+      serve("/query").with(DeprecatedChangeQueryServlet.class);
+    }
   }
 
   private Key<HttpServlet> notFound() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
index 53dee84..0f223c9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -52,14 +52,17 @@
 
 public class WebModule extends FactoryModule {
   private final AuthConfig authConfig;
+  private final UrlModule.UrlConfig urlConfig;
   private final boolean wantSSL;
   private final GitWebConfig gitWebConfig;
 
   @Inject
   WebModule(final AuthConfig authConfig,
+      final UrlModule.UrlConfig urlConfig,
       @CanonicalWebUrl @Nullable final String canonicalUrl,
       final Injector creatingInjector) {
     this.authConfig = authConfig;
+    this.urlConfig = urlConfig;
     this.wantSSL = canonicalUrl != null && canonicalUrl.startsWith("https:");
 
     this.gitWebConfig =
@@ -117,7 +120,7 @@
         throw new ProvisionException("Unsupported loginType: " + authConfig.getAuthType());
     }
 
-    install(new UrlModule());
+    install(new UrlModule(urlConfig));
     install(new UiRpcModule());
     install(new GerritRequestModule());
     install(new GitOverHttpServlet.Module());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
similarity index 94%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
index 9c4c598..cf443e7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd;
+package com.google.gerrit.httpd.rpc.change;
 
 import com.google.gerrit.server.query.change.QueryProcessor;
 import com.google.gerrit.server.query.change.QueryProcessor.OutputFormat;
@@ -29,12 +29,12 @@
 import javax.servlet.http.HttpServletResponse;
 
 @Singleton
-public class ChangeQueryServlet extends HttpServlet {
+public class DeprecatedChangeQueryServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
   private final Provider<QueryProcessor> processor;
 
   @Inject
-  ChangeQueryServlet(Provider<QueryProcessor> processor) {
+  DeprecatedChangeQueryServlet(Provider<QueryProcessor> processor) {
     this.processor = processor;
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java
new file mode 100644
index 0000000..91fc5b0
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java
@@ -0,0 +1,83 @@
+// 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.httpd.rpc.change;
+
+import com.google.gerrit.httpd.RestApiServlet;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ListChanges;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class ListChangesServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+  private static final Logger log = LoggerFactory.getLogger(ListChangesServlet.class);
+  private final ParameterParser paramParser;
+  private final Provider<ListChanges> factory;
+
+  @Inject
+  ListChangesServlet(ParameterParser paramParser, Provider<ListChanges> ls) {
+    this.paramParser = paramParser;
+    this.factory = ls;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse res)
+      throws IOException {
+    ListChanges impl = factory.get();
+    if (acceptsJson(req)) {
+      impl.setFormat(OutputFormat.JSON_COMPACT);
+    }
+    if (paramParser.parse(impl, req, res)) {
+      ByteArrayOutputStream buf = new ByteArrayOutputStream();
+      if (impl.getFormat().isJson()) {
+        buf.write(JSON_MAGIC);
+      }
+
+      Writer out = new BufferedWriter(new OutputStreamWriter(buf, "UTF-8"));
+      try {
+        impl.query(out);
+      } catch (QueryParseException e) {
+        res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+        sendText(req, res, e.getMessage());
+        return;
+      } catch (OrmException e) {
+        log.error("Error querying /changes/", e);
+        res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+        return;
+      }
+      out.flush();
+
+      res.setContentType(impl.getFormat().isJson() ? JSON_TYPE : "text/plain");
+      res.setCharacterEncoding("UTF-8");
+      send(req, res, buf.toByteArray());
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
index 47a9395..ab266f3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailFactory.java
@@ -134,7 +134,7 @@
     detail.setCanEdit(control.getRefControl().canWrite());
 
     if (detail.getChange().getStatus().isOpen()) {
-      List<SubmitRecord> submitRecords = control.canSubmit(db, patch.getId());
+      List<SubmitRecord> submitRecords = control.canSubmit(db, patch);
       for (SubmitRecord rec : submitRecords) {
         if (rec.labels != null) {
           for (SubmitRecord.Label lbl : rec.labels) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
index 638bfe3..183b5f6 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
@@ -83,7 +83,8 @@
     final Change.Id changeId = patchSetId.getParentKey();
     final ChangeControl control = changeControlFactory.validateFor(changeId);
     change = control.getChange();
-    patchSetInfo = infoFactory.get(db, patchSetId);
+    PatchSet patchSet = db.patchSets().get(patchSetId);
+    patchSetInfo = infoFactory.get(change, patchSet);
     drafts = db.patchComments().draftByPatchSetAuthor(patchSetId, user.getAccountId()).toList();
 
     aic.want(change.getOwner());
@@ -119,7 +120,7 @@
           .toList();
 
       boolean couldSubmit = false;
-      List<SubmitRecord> submitRecords = control.canSubmit(db, patchSetId);
+      List<SubmitRecord> submitRecords = control.canSubmit(db, patchSet);
       for (SubmitRecord rec : submitRecords) {
         if (rec.status == SubmitRecord.Status.OK) {
           couldSubmit = true;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
index 073e2d7..a3a1c25 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
@@ -18,11 +18,13 @@
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -34,6 +36,7 @@
 
 import java.io.IOException;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.Set;
 
 class DeleteBranches extends Handler<Set<Branch.NameKey>> {
@@ -50,6 +53,7 @@
   private final ReplicationQueue replication;
   private final IdentifiedUser identifiedUser;
   private final ChangeHooks hooks;
+  private final ReviewDb db;
 
   private final Project.NameKey projectName;
   private final Set<Branch.NameKey> toRemove;
@@ -60,6 +64,7 @@
       final ReplicationQueue replication,
       final IdentifiedUser identifiedUser,
       final ChangeHooks hooks,
+      final ReviewDb db,
 
       @Assisted Project.NameKey name, @Assisted Set<Branch.NameKey> toRemove) {
     this.projectControlFactory = projectControlFactory;
@@ -67,6 +72,7 @@
     this.replication = replication;
     this.identifiedUser = identifiedUser;
     this.hooks = hooks;
+    this.db = db;
 
     this.projectName = name;
     this.toRemove = toRemove;
@@ -74,17 +80,23 @@
 
   @Override
   public Set<Branch.NameKey> call() throws NoSuchProjectException,
-      RepositoryNotFoundException {
+      RepositoryNotFoundException, OrmException {
     final ProjectControl projectControl =
         projectControlFactory.controlFor(projectName);
 
-    for (Branch.NameKey k : toRemove) {
+    final Iterator<Branch.NameKey> branchIt = toRemove.iterator();
+    while (branchIt.hasNext()) {
+      final Branch.NameKey k = branchIt.next();
       if (!projectName.equals(k.getParentKey())) {
         throw new IllegalArgumentException("All keys must be from same project");
       }
       if (!projectControl.controlForRef(k).canDelete()) {
         throw new IllegalStateException("Cannot delete " + k.getShortName());
       }
+
+      if (db.changes().byBranchOpenAll(k).iterator().hasNext()) {
+        branchIt.remove();
+      }
     }
 
     final Set<Branch.NameKey> deleted = new HashSet<Branch.NameKey>();
diff --git a/gerrit-launcher/.gitignore b/gerrit-launcher/.gitignore
index 194bedc..980a6b1 100644
--- a/gerrit-launcher/.gitignore
+++ b/gerrit-launcher/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-launcher.iml
\ No newline at end of file
diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml
index 7d07652..e700351 100644
--- a/gerrit-launcher/pom.xml
+++ b/gerrit-launcher/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-launcher</artifactId>
diff --git a/gerrit-main/.gitignore b/gerrit-main/.gitignore
index 194bedc..c847710 100644
--- a/gerrit-main/.gitignore
+++ b/gerrit-main/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-main.iml
\ No newline at end of file
diff --git a/gerrit-main/pom.xml b/gerrit-main/pom.xml
index 9d8320c..bb2d763 100644
--- a/gerrit-main/pom.xml
+++ b/gerrit-main/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-main</artifactId>
diff --git a/gerrit-openid/.gitignore b/gerrit-openid/.gitignore
index 194bedc..158faf1 100644
--- a/gerrit-openid/.gitignore
+++ b/gerrit-openid/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-openid.iml
\ No newline at end of file
diff --git a/gerrit-openid/pom.xml b/gerrit-openid/pom.xml
index ed2625e..fa4ab95 100644
--- a/gerrit-openid/pom.xml
+++ b/gerrit-openid/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-openid</artifactId>
diff --git a/gerrit-patch-commonsnet/.gitignore b/gerrit-patch-commonsnet/.gitignore
index 194bedc..121f8e90 100644
--- a/gerrit-patch-commonsnet/.gitignore
+++ b/gerrit-patch-commonsnet/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-patch-commonsnet.iml
\ No newline at end of file
diff --git a/gerrit-patch-commonsnet/pom.xml b/gerrit-patch-commonsnet/pom.xml
index 75ee12e..f1a8b3e 100644
--- a/gerrit-patch-commonsnet/pom.xml
+++ b/gerrit-patch-commonsnet/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-commonsnet</artifactId>
diff --git a/gerrit-patch-jgit/.gitignore b/gerrit-patch-jgit/.gitignore
index 194bedc..7c4c433 100644
--- a/gerrit-patch-jgit/.gitignore
+++ b/gerrit-patch-jgit/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-patch-jgit.iml
\ No newline at end of file
diff --git a/gerrit-patch-jgit/pom.xml b/gerrit-patch-jgit/pom.xml
index f8190f5..65223fb 100644
--- a/gerrit-patch-jgit/pom.xml
+++ b/gerrit-patch-jgit/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-jgit</artifactId>
diff --git a/gerrit-pgm/.gitignore b/gerrit-pgm/.gitignore
index 194bedc..dafe355 100644
--- a/gerrit-pgm/.gitignore
+++ b/gerrit-pgm/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-pgm.iml
\ No newline at end of file
diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml
index 1463d15..a015219 100644
--- a/gerrit-pgm/pom.xml
+++ b/gerrit-pgm/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-pgm</artifactId>
diff --git a/gerrit-prettify/.gitignore b/gerrit-prettify/.gitignore
index 194bedc..8cf95ef 100644
--- a/gerrit-prettify/.gitignore
+++ b/gerrit-prettify/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-prettify.iml
\ No newline at end of file
diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml
index f5bd3d6..9354274 100644
--- a/gerrit-prettify/pom.xml
+++ b/gerrit-prettify/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-prettify</artifactId>
diff --git a/gerrit-reviewdb/.gitignore b/gerrit-reviewdb/.gitignore
index 194bedc..812ddd0 100644
--- a/gerrit-reviewdb/.gitignore
+++ b/gerrit-reviewdb/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-reviewdb.iml
\ No newline at end of file
diff --git a/gerrit-reviewdb/pom.xml b/gerrit-reviewdb/pom.xml
index 24d6a1b..f9fb49e 100644
--- a/gerrit-reviewdb/pom.xml
+++ b/gerrit-reviewdb/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-reviewdb</artifactId>
diff --git a/gerrit-server/.gitignore b/gerrit-server/.gitignore
index 194bedc..9324efe 100644
--- a/gerrit-server/.gitignore
+++ b/gerrit-server/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-server.iml
\ No newline at end of file
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index 58e43cf..f35608c 100644
--- a/gerrit-server/pom.xml
+++ b/gerrit-server/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-server</artifactId>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index 8ab9471..b9f476b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -16,12 +16,16 @@
 
 import static com.google.gerrit.rules.StoredValue.create;
 
+import com.google.common.collect.Maps;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -29,6 +33,7 @@
 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.query.change.ChangeData;
 
 import com.googlecode.prolog_cafe.lang.Prolog;
 import com.googlecode.prolog_cafe.lang.SystemException;
@@ -37,21 +42,25 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 
+import java.util.Map;
+
 public final class StoredValues {
   public static final StoredValue<ReviewDb> REVIEW_DB = create(ReviewDb.class);
   public static final StoredValue<Change> CHANGE = create(Change.class);
-  public static final StoredValue<PatchSet.Id> PATCH_SET_ID = create(PatchSet.Id.class);
+  public static final StoredValue<ChangeData> CHANGE_DATA = create(ChangeData.class);
+  public static final StoredValue<PatchSet> PATCH_SET = create(PatchSet.class);
   public static final StoredValue<ChangeControl> CHANGE_CONTROL = create(ChangeControl.class);
 
   public static final StoredValue<PatchSetInfo> PATCH_SET_INFO = new StoredValue<PatchSetInfo>() {
     @Override
     public PatchSetInfo createValue(Prolog engine) {
-      PatchSet.Id patchSetId = StoredValues.PATCH_SET_ID.get(engine);
+      Change change = StoredValues.CHANGE.get(engine);
+      PatchSet ps = StoredValues.PATCH_SET.get(engine);
       PrologEnvironment env = (PrologEnvironment) engine.control;
       PatchSetInfoFactory patchInfoFactory =
           env.getInjector().getInstance(PatchSetInfoFactory.class);
       try {
-        return patchInfoFactory.get(REVIEW_DB.get(engine), patchSetId);
+        return patchInfoFactory.get(change, ps);
       } catch (PatchSetInfoNotAvailableException e) {
         throw new SystemException(e.getMessage());
       }
@@ -102,6 +111,23 @@
     }
   };
 
+  public static final StoredValue<AnonymousUser> ANONYMOUS_USER =
+      new StoredValue<AnonymousUser>() {
+        @Override
+        protected AnonymousUser createValue(Prolog engine) {
+          PrologEnvironment env = (PrologEnvironment) engine.control;
+          return env.getInjector().getInstance(AnonymousUser.class);
+        }
+      };
+
+  public static final StoredValue<Map<Account.Id, IdentifiedUser>> USERS =
+      new StoredValue<Map<Account.Id, IdentifiedUser>>() {
+        @Override
+        protected Map<Account.Id, IdentifiedUser> createValue(Prolog engine) {
+          return Maps.newHashMap();
+        }
+      };
+
   private StoredValues() {
   }
 }
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 6c216a8..e469c34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -426,6 +426,56 @@
     }
   }
 
+  /**
+   * Unlink an authentication identity from an existing account.
+   *
+   * @param from account to unlink the identity from.
+   * @param who the identity to delete
+   * @return the result of unlinking the identity from the user.
+   * @throws AccountException the identity belongs to a different account, or it
+   *         cannot be unlinked at this time.
+   */
+  public AuthResult unlink(final Account.Id from, AuthRequest who)
+      throws AccountException {
+    try {
+      final ReviewDb db = schema.open();
+      try {
+        who = realm.unlink(db, from, who);
+
+        final AccountExternalId.Key key = id(who);
+        AccountExternalId extId = db.accountExternalIds().get(key);
+        if (extId != null) {
+          if (!extId.getAccountId().equals(from)) {
+            throw new AccountException("Identity in use by another account");
+          }
+          db.accountExternalIds().delete(Collections.singleton(extId));
+
+          if (who.getEmailAddress() != null) {
+            final Account a = db.accounts().get(from);
+            if (a.getPreferredEmail() != null
+                && a.getPreferredEmail().equals(who.getEmailAddress())) {
+              a.setPreferredEmail(null);
+              db.accounts().update(Collections.singleton(a));
+            }
+            byEmailCache.evict(who.getEmailAddress());
+            byIdCache.evict(from);
+          }
+
+        } else {
+          throw new AccountException("Identity not found");
+        }
+
+        return new AuthResult(from, key, false);
+
+      } finally {
+        db.close();
+      }
+    } catch (OrmException e) {
+      throw new AccountException("Cannot unlink identity", e);
+    }
+  }
+
+
   private static AccountExternalId.Key id(final AuthRequest who) {
     return new AccountExternalId.Key(who.getExternalId());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index 4f3392c..844e604 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
@@ -56,6 +56,11 @@
   }
 
   @Override
+  public AuthRequest unlink(ReviewDb db, Account.Id from, AuthRequest who) {
+    return who;
+  }
+
+  @Override
   public void onCreateAccount(final AuthRequest who, final Account account) {
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
index fc7c0be..2ebd0e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Realm.java
@@ -29,6 +29,9 @@
   public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who)
       throws AccountException;
 
+  public AuthRequest unlink(ReviewDb db, Account.Id to, AuthRequest who)
+      throws AccountException;
+
   public void onCreateAccount(AuthRequest who, Account account);
 
   public GroupMembership groups(AccountState who);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index e085d1e..910bf06 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
@@ -255,6 +255,11 @@
   }
 
   @Override
+  public AuthRequest unlink(ReviewDb db, Account.Id from, AuthRequest who) {
+    return who;
+  }
+
+  @Override
   public void onCreateAccount(final AuthRequest who, final Account account) {
     usernameCache.put(who.getLocalUser(), account.getId());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java
index abd3582..6648c7b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java
@@ -80,7 +80,7 @@
       throw new NoSuchChangeException(changeId);
     }
 
-    List<SubmitRecord> submitResult = control.canSubmit(db, patchSetId);
+    List<SubmitRecord> submitResult = control.canSubmit(db, patch);
     if (submitResult.isEmpty()) {
       throw new IllegalStateException(
           "ChangeControl.canSubmit returned empty list");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
index e76249a..6068c50 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -16,27 +16,10 @@
 
 import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
 
-import com.google.common.base.Function;
-import com.google.common.base.Predicates;
-import com.google.common.collect.Iterables;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
-import com.google.gwtorm.server.SchemaFactory;
-
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-
 import java.lang.reflect.InvocationTargetException;
-import java.text.MessageFormat;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -303,83 +286,6 @@
     }
   }
 
-  /**
-   * Resolve groups from group names, via the database. Group names not found in
-   * the database will be skipped.
-   *
-   * @param dbfactory database to resolve from.
-   * @param groupNames group names to resolve.
-   * @param log log for any warnings and errors.
-   * @param groupNotFoundWarning formatted message to output to the log for each
-   *        group name which is not found in the database. <code>{0}</code> will
-   *        be replaced with the group name.
-   * @return the actual groups resolved from the database. If no groups are
-   *         found, returns an empty {@code Set}, never {@code null}.
-   */
-  public static Set<AccountGroup.UUID> groupsFor(
-      SchemaFactory<ReviewDb> dbfactory, String[] groupNames, Logger log,
-      String groupNotFoundWarning) {
-    final Set<AccountGroup.UUID> result = new HashSet<AccountGroup.UUID>();
-    try {
-      final ReviewDb db = dbfactory.open();
-      try {
-        List<AccountGroupName> groups = db.accountGroupNames().get(
-            Iterables.transform(Arrays.asList(groupNames),
-                new Function<String, AccountGroup.NameKey>() {
-                  @Override
-                  public AccountGroup.NameKey apply(String name) {
-                    return new AccountGroup.NameKey(name);
-                  }
-            })).toList();
-
-        Iterator<AccountGroup> ags = db.accountGroups().get(
-            Iterables.transform(Iterables.filter(groups, Predicates.notNull()),
-                new Function<AccountGroupName, AccountGroup.Id>() {
-                  @Override
-                  public AccountGroup.Id apply(AccountGroupName group) {
-                    return group.getId();
-                  }
-            })).iterator();
-
-        for (int i = 0; i < groupNames.length; i++) {
-          if (groups.get(i) == null) {
-            log.warn(MessageFormat.format(groupNotFoundWarning, groupNames[i]));
-            continue;
-          }
-          AccountGroup ag = ags.next();
-          if (ag == null) {
-            log.warn(MessageFormat.format(groupNotFoundWarning, groupNames[i]));
-          } else {
-            result.add(ag.getGroupUUID());
-          }
-        }
-      } finally {
-        db.close();
-      }
-    } catch (OrmRuntimeException e) {
-      log.error("Database error, cannot load groups", e);
-    } catch (OrmException e) {
-      log.error("Database error, cannot load groups", e);
-    }
-    return result;
-  }
-
-  /**
-   * Resolve groups from group names, via the database. Group names not found in
-   * the database will be skipped.
-   *
-   * @param dbfactory database to resolve from.
-   * @param groupNames group names to resolve.
-   * @param log log for any warnings and errors.
-   * @return the actual groups resolved from the database. If no groups are
-   *         found, returns an empty {@code Set}, never {@code null}.
-   */
-  public static Set<AccountGroup.UUID> groupsFor(
-      SchemaFactory<ReviewDb> dbfactory, String[] groupNames, Logger log) {
-    return groupsFor(dbfactory, groupNames, log,
-        "Group \"{0}\" not in database, skipping.");
-  }
-
   private static boolean match(final String a, final String... cases) {
     for (final String b : cases) {
       if (equalsIgnoreCase(a, b)) {
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 944bbeb..f581adc 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
@@ -35,6 +35,7 @@
 import com.google.gerrit.server.changedetail.RestoreChange;
 import com.google.gerrit.server.changedetail.Submit;
 import com.google.gerrit.server.git.AsyncReceiveCommits;
+import com.google.gerrit.server.git.BanCommit;
 import com.google.gerrit.server.git.CreateCodeReviewNotes;
 import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MetaDataUpdate;
@@ -117,5 +118,6 @@
     factory(CreateProject.Factory.class);
     factory(Submit.Factory.class);
     factory(SuggestParentCandidates.Factory.class);
+    factory(BanCommit.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
index 9992f18..c89f025 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
@@ -15,8 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -25,9 +24,9 @@
 
 public class GitReceivePackGroupsProvider extends GroupSetProvider {
   @Inject
-  public GitReceivePackGroupsProvider(@GerritServerConfig Config config,
-      SchemaFactory<ReviewDb> db) {
-    super(config, db, "receive", null, "allowGroup");
+  public GitReceivePackGroupsProvider(GroupCache gc,
+      @GerritServerConfig Config config) {
+    super(gc, config, "receive", null, "allowGroup");
 
     // If no group was set, default to "registered users"
     //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
index 76d8844..b5de742 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
@@ -15,8 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -26,9 +25,9 @@
 
 public class GitUploadPackGroupsProvider extends GroupSetProvider {
   @Inject
-  public GitUploadPackGroupsProvider(@GerritServerConfig Config config,
-      SchemaFactory<ReviewDb> db) {
-    super(config, db, "upload", null, "allowGroup");
+  public GitUploadPackGroupsProvider(GroupCache gc,
+      @GerritServerConfig Config config) {
+    super(gc, config, "upload", null, "allowGroup");
 
     // If no group was set, default to "registered users" and "anonymous"
     //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
index 15711af..3619cda 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -14,12 +14,9 @@
 
 package com.google.gerrit.server.config;
 
-import static com.google.gerrit.server.config.ConfigUtil.groupsFor;
-import static java.util.Collections.unmodifiableSet;
-
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -37,10 +34,20 @@
   protected Set<AccountGroup.UUID> groupIds;
 
   @Inject
-  protected GroupSetProvider(@GerritServerConfig Config config,
-      SchemaFactory<ReviewDb> db, String section, String subsection, String name) {
+  protected GroupSetProvider(GroupCache groupCache,
+      @GerritServerConfig Config config, String section,
+      String subsection, String name) {
     String[] groupNames = config.getStringList(section, subsection, name);
-    groupIds = unmodifiableSet(groupsFor(db, groupNames, log));
+    ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
+    for (String n : groupNames) {
+      AccountGroup g = groupCache.get(new AccountGroup.NameKey(n));
+      if (g != null) {
+        builder.add(g.getGroupUUID());
+      } else {
+        log.warn("Group \"{0}\" not in database, skipping.", n);
+      }
+    }
+    groupIds = builder.build();
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
index b279086..7172b6f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
@@ -14,8 +14,7 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -33,8 +32,8 @@
  */
 public class ProjectOwnerGroupsProvider extends GroupSetProvider {
   @Inject
-  public ProjectOwnerGroupsProvider(
-      @GerritServerConfig final Config config, final SchemaFactory<ReviewDb> db) {
-    super(config, db, "repository", "*", "ownerGroup");
+  public ProjectOwnerGroupsProvider(GroupCache gc,
+      @GerritServerConfig final Config config) {
+    super(gc, config, "repository", "*", "ownerGroup");
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
new file mode 100644
index 0000000..c9c9753
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
@@ -0,0 +1,273 @@
+// 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;
+
+import static com.google.gerrit.server.git.GitRepositoryManager.REF_REJECT_COMMITS;
+
+import com.google.gerrit.common.errors.PermissionDeniedException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.notes.NoteMapMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.util.List;
+
+public class BanCommit {
+
+  private static final int MAX_LOCK_FAILURE_CALLS = 10;
+  private static final int SLEEP_ON_LOCK_FAILURE_MS = 25;
+
+  public interface Factory {
+    BanCommit create();
+  }
+
+  private final Provider<CurrentUser> currentUser;
+  private final GitRepositoryManager repoManager;
+  private final AccountCache accountCache;
+  private final PersonIdent gerritIdent;
+
+  @Inject
+  BanCommit(final Provider<CurrentUser> currentUser,
+      final GitRepositoryManager repoManager, final AccountCache accountCache,
+      @GerritPersonIdent final PersonIdent gerritIdent) {
+    this.currentUser = currentUser;
+    this.repoManager = repoManager;
+    this.accountCache = accountCache;
+    this.gerritIdent = gerritIdent;
+  }
+
+  public BanCommitResult ban(final ProjectControl projectControl,
+      final List<ObjectId> commitsToBan, final String reason)
+      throws PermissionDeniedException, IOException,
+      IncompleteUserInfoException, InterruptedException, MergeException {
+    if (!projectControl.isOwner()) {
+      throw new PermissionDeniedException(
+          "No project owner: not permitted to ban commits");
+    }
+
+    final BanCommitResult result = new BanCommitResult();
+
+    final PersonIdent currentUserIdent = createPersonIdent();
+    final Repository repo =
+        repoManager.openRepository(projectControl.getProject().getNameKey());
+    try {
+      final RevWalk revWalk = new RevWalk(repo);
+      final ObjectInserter inserter = repo.newObjectInserter();
+      try {
+        NoteMap baseNoteMap = null;
+        RevCommit baseCommit = null;
+        final Ref notesBranch = repo.getRef(REF_REJECT_COMMITS);
+        if (notesBranch != null) {
+          baseCommit = revWalk.parseCommit(notesBranch.getObjectId());
+          baseNoteMap = NoteMap.read(revWalk.getObjectReader(), baseCommit);
+        }
+
+        final NoteMap ourNoteMap;
+        if (baseCommit != null) {
+          ourNoteMap = NoteMap.read(repo.newObjectReader(), baseCommit);
+        } else {
+          ourNoteMap = NoteMap.newEmptyMap();
+        }
+
+        for (final ObjectId commitToBan : commitsToBan) {
+          try {
+            revWalk.parseCommit(commitToBan);
+          } catch (MissingObjectException e) {
+            // ignore exception, also not existing commits can be banned
+          } catch (IncorrectObjectTypeException e) {
+            result.notACommit(commitToBan, e.getMessage());
+            continue;
+          }
+
+          final Note note = ourNoteMap.getNote(commitToBan);
+          if (note != null) {
+            result.commitAlreadyBanned(commitToBan);
+            continue;
+          }
+
+          final String noteContent = reason != null ? reason : "";
+          final ObjectId noteContentId =
+              inserter
+                  .insert(Constants.OBJ_BLOB, noteContent.getBytes("UTF-8"));
+          ourNoteMap.set(commitToBan, noteContentId);
+          result.commitBanned(commitToBan);
+        }
+
+        if (result.getNewlyBannedCommits().isEmpty()) {
+          return result;
+        }
+
+        final ObjectId ourCommit =
+            commit(ourNoteMap, inserter, currentUserIdent, baseCommit, result,
+                reason);
+
+        updateRef(repo, revWalk, inserter, ourNoteMap, ourCommit, baseNoteMap,
+            baseCommit);
+      } finally {
+        revWalk.release();
+        inserter.release();
+      }
+    } finally {
+      repo.close();
+    }
+
+    return result;
+  }
+
+  private PersonIdent createPersonIdent() throws IncompleteUserInfoException {
+    final String userName = currentUser.get().getUserName();
+    final Account account = accountCache.getByUsername(userName).getAccount();
+    if (account.getFullName() == null) {
+      throw new IncompleteUserInfoException(userName, "full name");
+    }
+    if (account.getPreferredEmail() == null) {
+      throw new IncompleteUserInfoException(userName, "preferred email");
+    }
+    return new PersonIdent(account.getFullName(), account.getPreferredEmail());
+  }
+
+  private static ObjectId commit(final NoteMap noteMap,
+      final ObjectInserter inserter, final PersonIdent personIdent,
+      final ObjectId baseCommit, final BanCommitResult result,
+      final String reason) throws IOException {
+    final String commitMsg =
+        buildCommitMessage(result.getNewlyBannedCommits(), reason);
+    if (baseCommit != null) {
+      return createCommit(noteMap, inserter, personIdent, commitMsg, baseCommit);
+    } else {
+      return createCommit(noteMap, inserter, personIdent, commitMsg);
+    }
+  }
+
+  private static ObjectId createCommit(final NoteMap noteMap,
+      final ObjectInserter inserter, final PersonIdent personIdent,
+      final String message, final ObjectId... parents) throws IOException {
+    final CommitBuilder b = new CommitBuilder();
+    b.setTreeId(noteMap.writeTree(inserter));
+    b.setAuthor(personIdent);
+    b.setCommitter(personIdent);
+    if (parents.length > 0) {
+      b.setParentIds(parents);
+    }
+    b.setMessage(message);
+    final ObjectId commitId = inserter.insert(b);
+    inserter.flush();
+    return commitId;
+  }
+
+  private static String buildCommitMessage(final List<ObjectId> bannedCommits,
+      final String reason) {
+    final StringBuilder commitMsg = new StringBuilder();
+    commitMsg.append("Banning ");
+    commitMsg.append(bannedCommits.size());
+    commitMsg.append(" ");
+    commitMsg.append(bannedCommits.size() == 1 ? "commit" : "commits");
+    commitMsg.append("\n\n");
+    if (reason != null) {
+      commitMsg.append("Reason: ");
+      commitMsg.append(reason);
+      commitMsg.append("\n\n");
+    }
+    commitMsg.append("The following commits are banned:\n");
+    final StringBuilder commitList = new StringBuilder();
+    for (final ObjectId c : bannedCommits) {
+      if (commitList.length() > 0) {
+        commitList.append(",\n");
+      }
+      commitList.append(c.getName());
+    }
+    commitMsg.append(commitList);
+    return commitMsg.toString();
+  }
+
+  public void updateRef(final Repository repo, final RevWalk revWalk,
+      final ObjectInserter inserter, final NoteMap ourNoteMap,
+      final ObjectId oursCommit, final NoteMap baseNoteMap,
+      final ObjectId baseCommit) throws IOException, InterruptedException,
+      MissingObjectException, IncorrectObjectTypeException,
+      CorruptObjectException, MergeException {
+
+    int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
+    RefUpdate refUpdate = createRefUpdate(repo, oursCommit, baseCommit);
+
+    for (;;) {
+      final Result result = refUpdate.update();
+
+      if (result == Result.LOCK_FAILURE) {
+        if (--remainingLockFailureCalls > 0) {
+          Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
+        } else {
+          throw new MergeException("Failed to lock the ref: "
+              + REF_REJECT_COMMITS);
+        }
+
+      } else if (result == Result.REJECTED) {
+        final RevCommit theirsCommit =
+            revWalk.parseCommit(refUpdate.getOldObjectId());
+        final NoteMap theirNoteMap =
+            NoteMap.read(revWalk.getObjectReader(), theirsCommit);
+        final NoteMapMerger merger = new NoteMapMerger(repo);
+        final NoteMap merged =
+            merger.merge(baseNoteMap, ourNoteMap, theirNoteMap);
+        final ObjectId mergeCommit =
+            createCommit(merged, inserter, gerritIdent,
+                "Merged note commits\n", oursCommit, theirsCommit);
+        refUpdate = createRefUpdate(repo, mergeCommit, theirsCommit);
+        remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
+
+      } else if (result == Result.IO_FAILURE) {
+        throw new IOException(
+            "Couldn't create commit reject notes because of IO_FAILURE");
+      } else {
+        break;
+      }
+    }
+  }
+
+  private static RefUpdate createRefUpdate(final Repository repo,
+      final ObjectId newObjectId, final ObjectId expectedOldObjectId)
+      throws IOException {
+    RefUpdate refUpdate = repo.updateRef(REF_REJECT_COMMITS);
+    refUpdate.setNewObjectId(newObjectId);
+    if (expectedOldObjectId == null) {
+      refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
+    } else {
+      refUpdate.setExpectedOldObjectId(expectedOldObjectId);
+    }
+    return refUpdate;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java
new file mode 100644
index 0000000..1b48455
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java
@@ -0,0 +1,54 @@
+// 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;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class BanCommitResult {
+
+  private final List<ObjectId> newlyBannedCommits = new LinkedList<ObjectId>();
+  private final List<ObjectId> alreadyBannedCommits = new LinkedList<ObjectId>();
+  private final List<ObjectId> ignoredObjectIds = new LinkedList<ObjectId>();
+
+  public BanCommitResult() {
+  }
+
+  public void commitBanned(final ObjectId commitId) {
+    newlyBannedCommits.add(commitId);
+  }
+
+  public void commitAlreadyBanned(final ObjectId commitId) {
+    alreadyBannedCommits.add(commitId);
+  }
+
+  public void notACommit(final ObjectId id, final String message) {
+    ignoredObjectIds.add(id);
+  }
+
+  public List<ObjectId> getNewlyBannedCommits() {
+    return newlyBannedCommits;
+  }
+
+  public List<ObjectId> getAlreadyBannedCommits() {
+    return alreadyBannedCommits;
+  }
+
+  public List<ObjectId> getIgnoredObjectIds() {
+    return ignoredObjectIds;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
new file mode 100644
index 0000000..204d777
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/IncompleteUserInfoException.java
@@ -0,0 +1,23 @@
+// 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;
+
+public class IncompleteUserInfoException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public IncompleteUserInfoException(final String userName, final String missingInfo) {
+    super("For the user \"" + userName + "\" " + missingInfo + " is not set.");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
index 44becb5..1997c13 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.git;
 
 /** Indicates the current branch's queue cannot be processed at this time. */
-class MergeException extends Exception {
+public class MergeException extends Exception {
   private static final long serialVersionUID = 1L;
 
   MergeException(final String msg) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
index 5c6cd25..0868749 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
@@ -343,10 +343,6 @@
 
       if (config.isMirror()) {
         for (final Ref ref : remote.values()) {
-          if (noPerms && GitRepositoryManager.REF_CONFIG.equals(ref.getName())) {
-            continue;
-          }
-
           if (!Constants.HEAD.equals(ref.getName())) {
             final RefSpec spec = matchDst(ref.getName());
             if (spec != null && !local.containsKey(spec.getSource())) {
@@ -360,16 +356,13 @@
 
     } else {
       for (final String src : delta) {
-        if (noPerms && GitRepositoryManager.REF_CONFIG.equals(src)) {
-          continue;
-        }
-
         final RefSpec spec = matchSrc(src);
         if (spec != null) {
           // If the ref still exists locally, send it, otherwise delete it.
           //
           Ref srcRef = local.get(src);
-          if (srcRef != null) {
+          if (srcRef != null &&
+              !(noPerms && GitRepositoryManager.REF_CONFIG.equals(src))) {
             send(cmds, spec, srcRef);
           } else if (config.isMirror()) {
             delete(cmds, spec);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
index 5cf5f7a..5bff0ad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
@@ -14,13 +14,15 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.ReplicationUser;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
-import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -99,11 +101,12 @@
   private final SchemaFactory<ReviewDb> database;
   private final ReplicationUser.Factory replicationUserFactory;
   private final GitRepositoryManager gitRepositoryManager;
+  private final GroupCache groupCache;
 
   @Inject
   PushReplication(final Injector i, final WorkQueue wq, final SitePaths site,
       final ReplicationUser.Factory ruf, final SchemaFactory<ReviewDb> db,
-      final GitRepositoryManager grm)
+      final GitRepositoryManager grm, GroupCache gc)
       throws ConfigInvalidException, IOException {
     injector = i;
     workQueue = wq;
@@ -111,6 +114,7 @@
     replicationUserFactory = ruf;
     gitRepositoryManager = grm;
     configs = allConfigs(site);
+    groupCache = gc;
   }
 
   @Override
@@ -195,7 +199,6 @@
         }
       }
 
-
       if (c.getPushRefSpecs().isEmpty()) {
         RefSpec spec = new RefSpec();
         spec = spec.setSourceDestination("refs/*", "refs/*");
@@ -204,7 +207,7 @@
       }
 
       r.add(new ReplicationConfig(injector, workQueue, c, cfg, database,
-          replicationUserFactory, gitRepositoryManager));
+          replicationUserFactory, gitRepositoryManager, groupCache));
     }
     return Collections.unmodifiableList(r);
   }
@@ -392,7 +395,8 @@
     ReplicationConfig(final Injector injector, final WorkQueue workQueue,
         final RemoteConfig rc, final Config cfg, SchemaFactory<ReviewDb> db,
         final ReplicationUser.Factory replicationUserFactory,
-        final GitRepositoryManager gitRepositoryManager) {
+        final GitRepositoryManager gitRepositoryManager,
+        GroupCache groupCache) {
 
       remote = rc;
       delay = Math.max(0, getInt(rc, cfg, "replicationdelay", 15));
@@ -406,8 +410,16 @@
           cfg.getStringList("remote", rc.getName(), "authGroup");
       final GroupMembership authGroups;
       if (authGroupNames.length > 0) {
-        authGroups = new ListGroupMembership(ConfigUtil.groupsFor(db, authGroupNames, //
-            log, "Group \"{0}\" not in database, removing from authGroup"));
+        ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
+        for (String name : authGroupNames) {
+          AccountGroup g = groupCache.get(new AccountGroup.NameKey(name));
+          if (g != null) {
+            builder.add(g.getGroupUUID());
+          } else {
+            log.warn("Group \"{0}\" not in database, removing from authGroup", name);
+          }
+        }
+        authGroups = new ListGroupMembership(builder.build());
       } else {
         authGroups = ReplicationUser.EVERYTHING_VISIBLE;
       }
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 290b162..83877d0 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.server.git.MultiProgressMonitor.UNKNOWN;
 
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.ApprovalType;
@@ -112,6 +114,34 @@
   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 =
+      "Please read the documentation and contact an administrator\n"
+          + "if you feel the configuration is incorrect";
+
+  private enum Error {
+        CONFIG_UPDATE("You are not allowed to perform this operation.\n"
+        + "Configuration changes can only be pushed by project owners\n"
+        + "who also have 'Push' rights on " + GitRepositoryManager.REF_CONFIG),
+        UPDATE("You are not allowed to perform this operation.\n"
+        + "To push into this reference you need 'Push' rights."),
+        DELETE("You need 'Push' rights with the 'Force Push'\n"
+            + "flag set to delete references."),
+        CODE_REVIEW("You need 'Push' rights to upload code review requests.\n"
+            + "Verify that you are pushing to the right branch."),
+        CREATE("You are not allowed to perform this operation.\n"
+            + "To create new references you need 'Create Reference' rights.");
+
+    private final String value;
+
+    Error(String value) {
+      this.value = value;
+    }
+
+    public String get() {
+      return value;
+    }
+  }
+
   interface Factory {
     ReceiveCommits create(ProjectControl projectControl, Repository repository);
   }
@@ -215,6 +245,7 @@
   private final SubmoduleOp.Factory subOpFactory;
 
   private final List<Message> messages = new ArrayList<Message>();
+  private ListMultimap<Error, String> errors = LinkedListMultimap.create();
   private Task newProgress;
   private Task replaceProgress;
   private Task closeProgress;
@@ -438,6 +469,14 @@
     doReplaces();
     replaceProgress.end();
 
+    if (!errors.isEmpty()) {
+      for (Error error : errors.keySet()) {
+        rp.sendMessage(buildError(error, errors.get(error)));
+      }
+      rp.sendMessage(String.format("User: %s", displayName(currentUser)));
+      rp.sendMessage(COMMAND_REJECTION_MESSAGE_FOOTER);
+    }
+
     for (final ReceiveCommand c : commands) {
       if (c.getResult() == Result.OK) {
         switch (c.getType()) {
@@ -502,6 +541,30 @@
     }
   }
 
+  private String buildError(Error error, List<String> branches) {
+    StringBuilder sb = new StringBuilder();
+    if (branches.size() == 1) {
+      sb.append("Branch ").append(branches.get(0)).append(":\n");
+      sb.append(error.get());
+      return sb.toString();
+    }
+    sb.append("Branches");
+    String delim = " ";
+    for (String branch : branches) {
+      sb.append(delim).append(branch);
+      delim = ", ";
+    }
+    return sb.append(":\n").append(error.get()).toString();
+  }
+
+  private static String displayName(IdentifiedUser user) {
+    String displayName = user.getUserName();
+    if (displayName == null) {
+      displayName = user.getAccount().getPreferredEmail();
+    }
+    return displayName;
+  }
+
   private Account.Id toAccountId(final String nameOrEmail) throws OrmException,
       NoSuchAccountException {
     final Account a = accountResolver.findByNameOrEmail(nameOrEmail);
@@ -630,6 +693,7 @@
       validateNewCommits(ctl, cmd);
       cmd.execute(rp);
     } else {
+      errors.put(Error.CREATE, ctl.getRefName());
       reject(cmd, "can not create new references");
     }
   }
@@ -644,6 +708,11 @@
       validateNewCommits(ctl, cmd);
       cmd.execute(rp);
     } else {
+      if (GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())) {
+        errors.put(Error.CONFIG_UPDATE, GitRepositoryManager.REF_CONFIG);
+      } else {
+        errors.put(Error.UPDATE, ctl.getRefName());
+      }
       reject(cmd, "can not update the reference as a fast forward");
     }
   }
@@ -672,7 +741,12 @@
     if (ctl.canDelete()) {
       cmd.execute(rp);
     } else {
-      reject(cmd, "can not delete references");
+      if (GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())) {
+        reject(cmd, "cannot delete project configuration");
+      } else {
+        errors.put(Error.DELETE, ctl.getRefName());
+        reject(cmd, "can not delete references");
+      }
     }
   }
 
@@ -769,7 +843,8 @@
         destBranchName.substring(0, split));
     destBranchCtl = projectControl.controlForRef(destBranch);
     if (!destBranchCtl.canUpload()) {
-      reject(cmd, "can not upload a change to this reference");
+      errors.put(Error.CODE_REVIEW, cmd.getRefName());
+      reject(cmd, "can not upload review");
       return;
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
index f59dcc3..2619e00 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -29,6 +28,7 @@
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
@@ -68,31 +68,39 @@
   }
 
   public PatchSetInfo get(ReviewDb db, PatchSet.Id patchSetId)
-    throws PatchSetInfoNotAvailableException {
-    Repository repo = null;
+      throws PatchSetInfoNotAvailableException {
     try {
       final PatchSet patchSet = db.patchSets().get(patchSetId);
       final Change change = db.changes().get(patchSet.getId().getParentKey());
-      final Project.NameKey projectKey = change.getProject();
-      repo = repoManager.openRepository(projectKey);
+      return get(change, patchSet);
+    } catch (OrmException e) {
+      throw new PatchSetInfoNotAvailableException(e);
+    }
+  }
+
+  public PatchSetInfo get(Change change, PatchSet patchSet)
+      throws PatchSetInfoNotAvailableException {
+    Repository repo;
+    try {
+      repo = repoManager.openRepository(change.getProject());
+    } catch (RepositoryNotFoundException e) {
+      throw new PatchSetInfoNotAvailableException(e);
+    }
+    try {
       final RevWalk rw = new RevWalk(repo);
       try {
         final RevCommit src =
             rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
-        PatchSetInfo info = get(src, patchSetId);
+        PatchSetInfo info = get(src, patchSet.getId());
         info.setParents(toParentInfos(src.getParents(), rw));
         return info;
       } finally {
         rw.release();
       }
-    } catch (OrmException e) {
-      throw new PatchSetInfoNotAvailableException(e);
     } catch (IOException e) {
       throw new PatchSetInfoNotAvailableException(e);
     } finally {
-      if (repo != null) {
-        repo.close();
-      }
+      repo.close();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
index 53301b9..7652bed 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ChangeControl.java
@@ -26,10 +26,11 @@
 import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.util.Providers;
 
 import com.googlecode.prolog_cafe.compiler.CompileException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
@@ -49,6 +50,8 @@
 import java.util.List;
 import java.util.Set;
 
+import javax.annotation.Nullable;
+
 
 /** Access control management for a user accessing a single change. */
 public class ChangeControl {
@@ -161,7 +164,7 @@
 
   /** Can this user see this change? */
   public boolean isVisible(ReviewDb db) throws OrmException {
-    if (change.getStatus() == Change.Status.DRAFT && !isDraftVisible(db)) {
+    if (change.getStatus() == Change.Status.DRAFT && !isDraftVisible(db, null)) {
       return false;
     }
     return isRefVisible();
@@ -174,7 +177,7 @@
 
   /** Can this user see the given patchset? */
   public boolean isPatchVisible(PatchSet ps, ReviewDb db) throws OrmException {
-    if (ps.isDraft() && !isDraftVisible(db)) {
+    if (ps.isDraft() && !isDraftVisible(db, null)) {
       return false;
     }
     return isVisible(db);
@@ -235,10 +238,20 @@
 
   /** Is this user a reviewer for the change? */
   public boolean isReviewer(ReviewDb db) throws OrmException {
+    return isReviewer(db, null);
+  }
+
+  /** Is this user a reviewer for the change? */
+  public boolean isReviewer(ReviewDb db, @Nullable ChangeData cd)
+      throws OrmException {
     if (getCurrentUser() instanceof IdentifiedUser) {
       final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
-      ResultSet<PatchSetApproval> results =
-        db.patchSetApprovals().byChange(change.getId());
+      Iterable<PatchSetApproval> results;
+      if (cd != null) {
+        results = cd.currentApprovals(Providers.of(db));
+      } else {
+        results = db.patchSetApprovals().byChange(change.getId());
+      }
       for (PatchSetApproval approval : results) {
         if (user.getAccountId().equals(approval.getAccountId())) {
           return true;
@@ -278,34 +291,39 @@
     return false;
   }
 
-  public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet.Id patchSetId) {
+  public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet) {
+    return canSubmit(db, patchSet, null, false);
+  }
+
+  public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet,
+      @Nullable ChangeData cd, boolean fastEvalLabels) {
     if (change.getStatus().isClosed()) {
       SubmitRecord rec = new SubmitRecord();
       rec.status = SubmitRecord.Status.CLOSED;
       return Collections.singletonList(rec);
     }
 
-    if (!patchSetId.equals(change.currentPatchSetId())) {
-      return ruleError("Patch set " + patchSetId + " is not current");
+    if (!patchSet.getId().equals(change.currentPatchSetId())) {
+      return ruleError("Patch set " + patchSet.getPatchSetId() + " is not current");
     }
 
     try {
-      if (change.getStatus() == Change.Status.DRAFT){
-        if (!isVisible(db)) {
-          return ruleError("Patch set " + patchSetId + " not found");
+      if (change.getStatus() == Change.Status.DRAFT) {
+        if (!isDraftVisible(db, cd)) {
+          return ruleError("Patch set " + patchSet.getPatchSetId() + " not found");
         } else {
           return ruleError("Cannot submit draft changes");
         }
       }
-      if (isDraftPatchSet(patchSetId, db)) {
-        if (!isVisible(db)) {
-          return ruleError("Patch set " + patchSetId + " not found");
+      if (patchSet.isDraft()) {
+        if (!isDraftVisible(db, cd)) {
+          return ruleError("Patch set " + patchSet.getPatchSetId() + " not found");
         } else {
           return ruleError("Cannot submit draft patch sets");
         }
       }
     } catch (OrmException err) {
-      return logRuleError("Cannot read patch set " + patchSetId, err);
+      return logRuleError("Cannot read patch set " + patchSet.getId(), err);
     }
 
     List<Term> results = new ArrayList<Term>();
@@ -323,7 +341,8 @@
     try {
       env.set(StoredValues.REVIEW_DB, db);
       env.set(StoredValues.CHANGE, change);
-      env.set(StoredValues.PATCH_SET_ID, patchSetId);
+      env.set(StoredValues.CHANGE_DATA, cd);
+      env.set(StoredValues.PATCH_SET, patchSet);
       env.set(StoredValues.CHANGE_CONTROL, this);
 
       submitRule = env.once(
@@ -334,6 +353,10 @@
             + getProject().getName());
       }
 
+      if (fastEvalLabels) {
+        env.once("gerrit", "assume_range_from_label");
+      }
+
       try {
         for (Term[] template : env.all(
             "gerrit", "can_submit",
@@ -372,6 +395,10 @@
             parentEnv.once("gerrit", "locate_submit_filter", new VariableTerm());
         if (filterRule != null) {
           try {
+            if (fastEvalLabels) {
+              env.once("gerrit", "assume_range_from_label");
+            }
+
             Term resultsTerm = toListTerm(results);
             results.clear();
             Term[] template = parentEnv.once(
@@ -515,16 +542,9 @@
     }
   }
 
-  private boolean isDraftVisible(ReviewDb db) throws OrmException {
-    return isOwner() || isReviewer(db);
-  }
-
-  private boolean isDraftPatchSet(PatchSet.Id id, ReviewDb db) throws OrmException {
-    PatchSet ps = db.patchSets().get(id);
-    if (ps == null) {
-      throw new OrmException("Patch set " + id + " not found");
-    }
-    return ps.isDraft();
+  private boolean isDraftVisible(ReviewDb db, ChangeData cd)
+      throws OrmException {
+    return isOwner() || isReviewer(db, cd);
   }
 
   private static boolean isUser(Term who) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 7a85b6f..db3470e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSet.Id;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.TrackingId;
@@ -28,7 +31,9 @@
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -46,9 +51,61 @@
 import java.util.Map;
 
 public class ChangeData {
+  public static void ensureChangeLoaded(
+      Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+    Map<Change.Id, ChangeData> missing = Maps.newHashMap();
+    for (ChangeData cd : changes) {
+      if (cd.change == null) {
+        missing.put(cd.getId(), cd);
+      }
+    }
+    if (!missing.isEmpty()) {
+      for (Change change : db.get().changes().get(missing.keySet())) {
+        missing.get(change.getId()).change = change;
+      }
+    }
+  }
+
+  public static void ensureCurrentPatchSetLoaded(
+      Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+    Map<PatchSet.Id, ChangeData> missing = Maps.newHashMap();
+    for (ChangeData cd : changes) {
+      if (cd.currentPatchSet == null && cd.patches == null) {
+        missing.put(cd.change(db).currentPatchSetId(), cd);
+      }
+    }
+    if (!missing.isEmpty()) {
+      for (PatchSet ps : db.get().patchSets().get(missing.keySet())) {
+        ChangeData cd = missing.get(ps.getId());
+        cd.currentPatchSet = ps;
+        cd.patches = Lists.newArrayList(ps);
+      }
+    }
+  }
+
+  public static void ensureCurrentApprovalsLoaded(
+      Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+    List<ResultSet<PatchSetApproval>> pending = Lists.newArrayList();
+    for (ChangeData cd : changes) {
+      if (cd.currentApprovals == null && cd.approvals == null) {
+        pending.add(db.get().patchSetApprovals()
+            .byPatchSet(cd.change(db).currentPatchSetId()));
+      }
+    }
+    if (!pending.isEmpty()) {
+      int idx = 0;
+      for (ChangeData cd : changes) {
+        if (cd.currentApprovals == null && cd.approvals == null) {
+          cd.currentApprovals = pending.get(idx++).toList();
+        }
+      }
+    }
+  }
+
   private final Change.Id legacyId;
   private Change change;
   private String commitMessage;
+  private PatchSet currentPatchSet;
   private Collection<PatchSet> patches;
   private Collection<PatchSetApproval> approvals;
   private Map<PatchSet.Id,Collection<PatchSetApproval>> approvalsMap;
@@ -57,6 +114,7 @@
   private Collection<PatchLineComment> comments;
   private Collection<TrackingId> trackingIds;
   private CurrentUser visibleTo;
+  private ChangeControl changeControl;
   private List<ChangeMessage> messages;
 
   public ChangeData(final Change.Id id) {
@@ -125,8 +183,13 @@
     return visibleTo == user;
   }
 
-  void cacheVisibleTo(CurrentUser user) {
-    visibleTo = user;
+  ChangeControl changeControl() {
+    return changeControl;
+  }
+
+  void cacheVisibleTo(ChangeControl ctl) {
+    visibleTo = ctl.getCurrentUser();
+    changeControl = ctl;
   }
 
   public Change change(Provider<ReviewDb> db) throws OrmException {
@@ -137,16 +200,19 @@
   }
 
   public PatchSet currentPatchSet(Provider<ReviewDb> db) throws OrmException {
-    Change c = change(db);
-    if (c == null) {
-      return null;
-    }
-    for (PatchSet p : patches(db)) {
-      if (p.getId().equals(c.currentPatchSetId())) {
-        return p;
+    if (currentPatchSet == null) {
+      Change c = change(db);
+      if (c == null) {
+        return null;
+      }
+      for (PatchSet p : patches(db)) {
+        if (p.getId().equals(c.currentPatchSetId())) {
+          currentPatchSet = p;
+          return p;
+        }
       }
     }
-    return null;
+    return currentPatchSet;
   }
 
   public Collection<PatchSetApproval> currentApprovals(Provider<ReviewDb> db)
@@ -155,24 +221,21 @@
       Change c = change(db);
       if (c == null) {
         currentApprovals = Collections.emptyList();
+      } else if (approvals != null) {
+        Map<Id, Collection<PatchSetApproval>> map = approvalsMap(db);
+        currentApprovals = map.get(c.currentPatchSetId());
+        if (currentApprovals == null) {
+          currentApprovals = Collections.emptyList();
+          map.put(c.currentPatchSetId(), currentApprovals);
+        }
       } else {
-        currentApprovals = approvalsFor(db, c.currentPatchSetId());
+        currentApprovals = db.get().patchSetApprovals()
+            .byPatchSet(c.currentPatchSetId()).toList();
       }
     }
     return currentApprovals;
   }
 
-  public Collection<PatchSetApproval> approvalsFor(Provider<ReviewDb> db,
-      PatchSet.Id psId) throws OrmException {
-    List<PatchSetApproval> r = new ArrayList<PatchSetApproval>();
-    for (PatchSetApproval p : approvals(db)) {
-      if (p.getPatchSetId().equals(psId)) {
-        r.add(p);
-      }
-    }
-    return r;
-  }
-
   public String commitMessage(GitRepositoryManager repoManager,
       Provider<ReviewDb> db) throws IOException, OrmException {
     if (commitMessage == null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
index 413e6c4..b73465a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
@@ -55,15 +55,18 @@
     }
     try {
       Change c = cd.change(db);
-      if (c != null && changeControl.controlFor(c, user).isVisible(db.get())) {
-        cd.cacheVisibleTo(user);
-        return true;
-      } else {
+      if (c == null) {
         return false;
       }
+
+      ChangeControl cc = changeControl.controlFor(c, user);
+      if (cc.isVisible(db.get())) {
+        cd.cacheVisibleTo(cc);
+        return true;
+      }
     } catch (NoSuchChangeException e) {
-      return false;
     }
+    return false;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java
new file mode 100644
index 0000000..6f9094a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java
@@ -0,0 +1,318 @@
+// 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.query.change;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.events.AccountAttribute;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class ListChanges {
+  private final QueryProcessor imp;
+  private final Provider<ReviewDb> db;
+  private final ApprovalTypes approvalTypes;
+  private final CurrentUser user;
+  private final ChangeControl.Factory changeControlFactory;
+  private boolean reverse;
+  private Map<Account.Id, AccountAttribute> accounts;
+
+  @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+  private OutputFormat format = OutputFormat.TEXT;
+
+  @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", multiValued = true, usage = "Query string")
+  private List<String> queries;
+
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "Maximum number of results to return")
+  void setLimit(int limit) {
+    imp.setLimit(limit);
+  }
+
+  @Option(name = "-P", metaVar = "SORTKEY", usage = "Previous changes before SORTKEY")
+  void setSortKeyAfter(String key) {
+    // Querying for the prior page of changes requires sortkey_after predicate.
+    // Changes are shown most recent->least recent. The previous page of
+    // results contains changes that were updated after the given key.
+    imp.setSortkeyAfter(key);
+    reverse = true;
+  }
+
+  @Option(name = "-N", metaVar = "SORTKEY", usage = "Next changes after SORTKEY")
+  void setSortKeyBefore(String key) {
+    // Querying for the next page of changes requires sortkey_before predicate.
+    // Changes are shown most recent->least recent. The next page contains
+    // changes that were updated before the given key.
+    imp.setSortkeyBefore(key);
+  }
+
+  @Inject
+  ListChanges(QueryProcessor qp,
+      Provider<ReviewDb> db,
+      ApprovalTypes at,
+      CurrentUser u,
+      ChangeControl.Factory cf) {
+    this.imp = qp;
+    this.db = db;
+    this.approvalTypes = at;
+    this.user = u;
+    this.changeControlFactory = cf;
+
+    accounts = Maps.newHashMap();
+  }
+
+  public OutputFormat getFormat() {
+    return format;
+  }
+
+  public ListChanges setFormat(OutputFormat fmt) {
+    this.format = fmt;
+    return this;
+  }
+
+  public void query(Writer out)
+      throws OrmException, QueryParseException, IOException {
+    if (imp.isDisabled()) {
+      throw new QueryParseException("query disabled");
+    }
+    if (queries == null || queries.isEmpty()) {
+      queries = Collections.singletonList("status:open");
+    } else if (queries.size() > 10) {
+      // Hard-code a default maximum number of queries to prevent
+      // users from submitting too much to the server in a single call.
+      throw new QueryParseException("limit of 10 queries");
+    }
+
+    List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(queries.size());
+    for (String query : queries) {
+      List<ChangeData> changes = imp.queryChanges(query);
+      boolean moreChanges = imp.getLimit() > 0 && changes.size() > imp.getLimit();
+      if (moreChanges) {
+        if (reverse) {
+          changes = changes.subList(1, changes.size());
+        } else {
+          changes = changes.subList(0, imp.getLimit());
+        }
+      }
+      ChangeData.ensureChangeLoaded(db, changes);
+      ChangeData.ensureCurrentPatchSetLoaded(db, changes);
+      ChangeData.ensureCurrentApprovalsLoaded(db, changes);
+
+      List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
+      for (ChangeData cd : changes) {
+        info.add(toChangeInfo(cd));
+      }
+      if (moreChanges && !info.isEmpty()) {
+        if (reverse) {
+          info.get(0)._moreChanges = true;
+        } else {
+          info.get(info.size() - 1)._moreChanges = true;
+        }
+      }
+      res.add(info);
+    }
+
+    if (!accounts.isEmpty()) {
+      for (Account account : db.get().accounts().get(accounts.keySet())) {
+        AccountAttribute a = accounts.get(account.getId());
+        a.name = Strings.emptyToNull(account.getFullName());
+      }
+    }
+
+    if (format.isJson()) {
+      format.newGson().toJson(
+          res.size() == 1 ? res.get(0) : res,
+          new TypeToken<List<ChangeInfo>>() {}.getType(),
+          out);
+      out.write('\n');
+    } else {
+      boolean firstQuery = true;
+      for (List<ChangeInfo> info : res) {
+        if (firstQuery) {
+          firstQuery = false;
+        } else {
+          out.write('\n');
+        }
+        for (ChangeInfo c : info) {
+          String id = new Change.Key(c.id).abbreviate();
+          String subject = c.subject;
+          if (subject.length() + id.length() > 80) {
+            subject = subject.substring(0, 80 - id.length());
+          }
+          out.write(id);
+          out.write(' ');
+          out.write(subject.replace('\n', ' '));
+          out.write('\n');
+        }
+      }
+    }
+  }
+
+  private ChangeInfo toChangeInfo(ChangeData cd) throws OrmException {
+    ChangeInfo out = new ChangeInfo();
+    Change in = cd.change(db);
+    out.project = in.getProject().get();
+    out.branch = in.getDest().getShortName();
+    out.topic = in.getTopic();
+    out.id = in.getKey().get();
+    out.subject = in.getSubject();
+    out.status = in.getStatus();
+    out.owner = asAccountAttribute(in.getOwner());
+    out.created = in.getCreatedOn();
+    out.updated = in.getLastUpdatedOn();
+    out._number = in.getId().get();
+    out._sortkey = in.getSortKey();
+    out.starred = user.getStarredChanges().contains(in.getId()) ? true : null;
+    out.labels = labelsFor(cd);
+    return out;
+  }
+
+  private AccountAttribute asAccountAttribute(Account.Id user) {
+    AccountAttribute a = accounts.get(user);
+    if (a == null) {
+      a = new AccountAttribute();
+      accounts.put(user, a);
+    }
+    return a;
+  }
+
+  private Map<String, LabelInfo> labelsFor(ChangeData cd) throws OrmException {
+    Change in = cd.change(db);
+    ChangeControl ctl = cd.changeControl();
+    if (ctl == null || ctl.getCurrentUser() != user) {
+      try {
+        ctl = changeControlFactory.controlFor(in);
+      } catch (NoSuchChangeException e) {
+        return null;
+      }
+    }
+
+    PatchSet ps = cd.currentPatchSet(db);
+    Map<String, LabelInfo> labels = Maps.newLinkedHashMap();
+    for (SubmitRecord rec : ctl.canSubmit(db.get(), ps, cd, true)) {
+      if (rec.labels == null) {
+        continue;
+      }
+      for (SubmitRecord.Label r : rec.labels) {
+        LabelInfo p = labels.get(r.label);
+        if (p == null || p._status.compareTo(r.status) < 0) {
+          LabelInfo n = new LabelInfo();
+          n._status = r.status;
+          switch (r.status) {
+            case OK:
+              n.approved = asAccountAttribute(r.appliedBy);
+              break;
+            case REJECT:
+              n.rejected = asAccountAttribute(r.appliedBy);
+              break;
+          }
+          labels.put(r.label, n);
+        }
+      }
+    }
+
+    Collection<PatchSetApproval> approvals = null;
+    for (Map.Entry<String, LabelInfo> e : labels.entrySet()) {
+      if (e.getValue().approved != null || e.getValue().rejected != null) {
+        continue;
+      }
+
+      ApprovalType type = approvalTypes.byLabel(e.getKey());
+      if (type == null || type.getMin() == null || type.getMax() == null) {
+        // Unknown or misconfigured type can't have intermediate scores.
+        continue;
+      }
+
+      short min = type.getMin().getValue();
+      short max = type.getMax().getValue();
+      if (-1 <= min && max <= 1) {
+        // Types with a range of -1..+1 can't have intermediate scores.
+        continue;
+      }
+
+      if (approvals == null) {
+        approvals = cd.currentApprovals(db);
+      }
+      for (PatchSetApproval psa : approvals) {
+        short val = psa.getValue();
+        if (val != 0 && min < val && val < max
+            && psa.getCategoryId().equals(type.getCategory().getId())) {
+          if (0 < val) {
+            e.getValue().recommended = asAccountAttribute(psa.getAccountId());
+            e.getValue().value = val != 1 ? val : null;
+          } else {
+            e.getValue().disliked = asAccountAttribute(psa.getAccountId());
+            e.getValue().value = val != -1 ? val : null;
+          }
+        }
+      }
+    }
+    return labels;
+  }
+
+  static class ChangeInfo {
+    String project;
+    String branch;
+    String topic;
+    String id;
+    String subject;
+    Change.Status status;
+    Timestamp created;
+    Timestamp updated;
+    Boolean starred;
+
+    String _sortkey;
+    int _number;
+
+    AccountAttribute owner;
+    Map<String, LabelInfo> labels;
+    Boolean _moreChanges;
+  }
+
+  static class LabelInfo {
+    transient SubmitRecord.Label.Status _status;
+    AccountAttribute approved;
+    AccountAttribute rejected;
+
+    AccountAttribute recommended;
+    AccountAttribute disliked;
+    Short value;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index a2fa7fe..76945f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -55,6 +55,30 @@
   private static final Logger log =
       LoggerFactory.getLogger(QueryProcessor.class);
 
+  private final Comparator<ChangeData> cmpAfter =
+      new Comparator<ChangeData>() {
+        @Override
+        public int compare(ChangeData a, ChangeData b) {
+          try {
+            return a.change(db).getSortKey().compareTo(b.change(db).getSortKey());
+          } catch (OrmException e) {
+            return 0;
+          }
+        }
+      };
+
+  private final Comparator<ChangeData> cmpBefore =
+      new Comparator<ChangeData>() {
+        @Override
+        public int compare(ChangeData a, ChangeData b) {
+          try {
+            return b.change(db).getSortKey().compareTo(a.change(db).getSortKey());
+          } catch (OrmException e) {
+            return 0;
+          }
+        }
+      };
+
   public static enum OutputFormat {
     TEXT, JSON;
   }
@@ -71,6 +95,9 @@
   private final int maxLimit;
 
   private OutputFormat outputFormat = OutputFormat.TEXT;
+  private int limit;
+  private String sortkeyAfter;
+  private String sortkeyBefore;
   private boolean includePatchSets;
   private boolean includeCurrentPatchSet;
   private boolean includeApprovals;
@@ -97,6 +124,22 @@
       .getMax();
   }
 
+  int getLimit() {
+    return limit;
+  }
+
+  void setLimit(int n) {
+    limit = n;
+  }
+
+  void setSortkeyAfter(String sortkey) {
+    sortkeyAfter = sortkey;
+  }
+
+  void setSortkeyBefore(String sortkey) {
+    sortkeyBefore = sortkey;
+  }
+
   public void setIncludePatchSets(boolean on) {
     includePatchSets = on;
   }
@@ -146,6 +189,14 @@
     this.outputFormat = fmt;
   }
 
+  /**
+   * Query for changes that match the query string.
+   * <p>
+   * If a limit was specified using {@link #setLimit(int)} this method may
+   * return up to {@code limit + 1} results, allowing the caller to determine if
+   * there are more than {@code limit} matches and suggest to its own caller
+   * that the query could be retried with {@link #setSortkeyBefore(String)}.
+   */
   public List<ChangeData> queryChanges(final String queryString)
       throws OrmException, QueryParseException {
     final Predicate<ChangeData> visibleToMe = queryBuilder.is_visible();
@@ -175,19 +226,14 @@
       }
     }
 
-    Collections.sort(results, new Comparator<ChangeData>() {
-      @Override
-      public int compare(ChangeData a, ChangeData b) {
-        return b.getChange().getSortKey().compareTo(
-            a.getChange().getSortKey());
-      }
-    });
-
+    Collections.sort(results, sortkeyAfter != null ? cmpAfter : cmpBefore);
     int limit = limit(s);
     if (limit < results.size()) {
       results = results.subList(0, limit);
     }
-
+    if (sortkeyAfter != null) {
+      Collections.reverse(results);
+    }
     return results;
   }
 
@@ -196,7 +242,7 @@
         new BufferedWriter( //
             new OutputStreamWriter(outputStream, "UTF-8")));
     try {
-      if (maxLimit <= 0) {
+      if (isDisabled()) {
         ErrorMessage m = new ErrorMessage();
         m.message = "query disabled";
         show(m);
@@ -233,7 +279,7 @@
             if (current != null) {
               c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
               eventFactory.addApprovals(c.currentPatchSet, //
-                  d.approvalsFor(db, current.getId()));
+                  d.currentApprovals(db));
 
               if (includeFiles) {
                 eventFactory.addPatchSetFileNames(c.currentPatchSet,
@@ -283,8 +329,13 @@
     }
   }
 
+  boolean isDisabled() {
+    return maxLimit <= 0;
+  }
+
   private int limit(Predicate<ChangeData> s) {
-    return queryBuilder.hasLimit(s) ? queryBuilder.getLimit(s) : maxLimit;
+    int n = queryBuilder.hasLimit(s) ? queryBuilder.getLimit(s) : maxLimit;
+    return limit > 0 ? Math.min(n, limit) + 1 : n;
   }
 
   @SuppressWarnings("unchecked")
@@ -293,9 +344,17 @@
 
     Predicate<ChangeData> q = queryBuilder.parse(queryString);
     if (!queryBuilder.hasSortKey(q)) {
-      q = Predicate.and(q, queryBuilder.sortkey_before("z"));
+      if (sortkeyBefore != null) {
+        q = Predicate.and(q, queryBuilder.sortkey_before(sortkeyBefore));
+      } else if (sortkeyAfter != null) {
+        q = Predicate.and(q, queryBuilder.sortkey_after(sortkeyAfter));
+      } else {
+        q = Predicate.and(q, queryBuilder.sortkey_before("z"));
+      }
     }
-    q = Predicate.and(q, queryBuilder.limit(maxLimit), visibleToMe);
+    q = Predicate.and(q,
+        queryBuilder.limit(limit > 0 ? Math.min(limit, maxLimit) + 1 : maxLimit),
+        visibleToMe);
 
     Predicate<ChangeData> s = queryRewriter.rewrite(q);
     if (!(s instanceof ChangeDataSource)) {
@@ -303,7 +362,7 @@
     }
 
     if (!(s instanceof ChangeDataSource)) {
-      throw new QueryParseException("cannot execute query: " + s);
+      throw new QueryParseException("invalid query: " + s);
     }
 
     return s;
diff --git a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
index c760426..a0bb820 100644
--- a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
@@ -9,7 +9,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.rules.PrologEnvironment;
 import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
+import com.google.inject.util.Providers;
 
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.JavaException;
@@ -44,10 +46,18 @@
     try {
       PrologEnvironment env = (PrologEnvironment) engine.control;
       ReviewDb db = StoredValues.REVIEW_DB.get(engine);
-      PatchSet.Id patchSetId = StoredValues.PATCH_SET_ID.get(engine);
+      PatchSet patchSet = StoredValues.PATCH_SET.get(engine);
+      ChangeData cd = StoredValues.CHANGE_DATA.getOrNull(engine);
       ApprovalTypes types = env.getInjector().getInstance(ApprovalTypes.class);
 
-      for (PatchSetApproval a : db.patchSetApprovals().byPatchSet(patchSetId)) {
+      Iterable<PatchSetApproval> approvals;
+      if (cd != null) {
+        approvals = cd.currentApprovals(Providers.of(db));
+      } else {
+        approvals = db.patchSetApprovals().byPatchSet(patchSet.getId());
+      }
+
+      for (PatchSetApproval a : approvals) {
         if (a.getValue() == 0) {
           continue;
         }
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
index 0a15608..1359de1 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
@@ -20,15 +20,12 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.rules.PrologEnvironment;
 import com.google.gerrit.rules.StoredValues;
-import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Provider;
+import com.google.inject.util.Providers;
 
-import com.googlecode.prolog_cafe.lang.HashtableOfTerm;
 import com.googlecode.prolog_cafe.lang.IllegalTypeException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
-import com.googlecode.prolog_cafe.lang.InternalException;
 import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.PInstantiationException;
@@ -39,6 +36,8 @@
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 
+import java.util.Map;
+
 /**
  * Loads a CurrentUser object for a user identity.
  * <p>
@@ -53,7 +52,6 @@
   private static final long serialVersionUID = 1L;
   private static final SymbolTerm user = intern("user", 1);
   private static final SymbolTerm anonymous = intern("anonymous");
-  private static final SymbolTerm current_user = intern("current_user");
 
   PRED_current_user_2(Term a1, Term a2, Operation n) {
     arg1 = a1;
@@ -71,24 +69,14 @@
       throw new PInstantiationException(this, 1);
     }
 
-    HashtableOfTerm userHash = userHash(engine);
-    Term userTerm = userHash.get(a1);
-    if (userTerm != null && userTerm.isJavaObject()) {
-      if (!(((JavaObjectTerm) userTerm).object() instanceof CurrentUser)) {
-        userTerm = createUser(engine, a1, userHash);
-      }
-    } else {
-      userTerm = createUser(engine, a1, userHash);
-    }
-
-    if (!a2.unify(userTerm, engine.trail)) {
+    if (!a2.unify(createUser(engine, a1), engine.trail)) {
       return engine.fail();
     }
 
     return cont;
   }
 
-  public Term createUser(Prolog engine, Term key, HashtableOfTerm userHash) {
+  public Term createUser(Prolog engine, Term key) {
     if (!key.isStructure()
         || key.arity() != 1
         || !((StructureTerm) key).functor().equals(user)) {
@@ -98,54 +86,30 @@
     Term idTerm = key.arg(0);
     CurrentUser user;
     if (idTerm.isInteger()) {
+      Map<Account.Id, IdentifiedUser> cache = StoredValues.USERS.get(engine);
       Account.Id accountId = new Account.Id(((IntegerTerm) idTerm).intValue());
-
-      final ReviewDb db = StoredValues.REVIEW_DB.getOrNull(engine);
-      IdentifiedUser.GenericFactory userFactory = userFactory(engine);
-      if (db != null) {
-        user = userFactory.create(new Provider<ReviewDb>() {
-          public ReviewDb get() {
-            return db;
-          }
-        }, accountId);
-      } else {
-        user = userFactory.create(accountId);
+      user = cache.get(accountId);
+      if (user == null) {
+        ReviewDb db = StoredValues.REVIEW_DB.getOrNull(engine);
+        IdentifiedUser.GenericFactory userFactory = userFactory(engine);
+        IdentifiedUser who;
+        if (db != null) {
+          who = userFactory.create(Providers.of(db), accountId);
+        } else {
+          who = userFactory.create(accountId);
+        }
+        cache.put(accountId, who);
+        user = who;
       }
 
-
     } else if (idTerm.equals(anonymous)) {
-      user = anonymousUser(engine);
+      user = StoredValues.ANONYMOUS_USER.get(engine);
 
     } else {
       throw new IllegalTypeException(this, 1, "user(int)", key);
     }
 
-    Term userTerm = new JavaObjectTerm(user);
-    userHash.put(key, userTerm);
-    return userTerm;
-  }
-
-  private static HashtableOfTerm userHash(Prolog engine) {
-    Term userHash = engine.getHashManager().get(current_user);
-    if (userHash == null) {
-      HashtableOfTerm users = new HashtableOfTerm();
-      engine.getHashManager().put(current_user, new JavaObjectTerm(userHash));
-      return users;
-    }
-
-    if (userHash.isJavaObject()) {
-      Object obj = ((JavaObjectTerm) userHash).object();
-      if (obj instanceof HashtableOfTerm) {
-        return (HashtableOfTerm) obj;
-      }
-    }
-
-    throw new InternalException(current_user + " is not HashtableOfTerm");
-  }
-
-  private static AnonymousUser anonymousUser(Prolog engine) {
-    PrologEnvironment env = (PrologEnvironment) engine.control;
-    return env.getInjector().getInstance(AnonymousUser.class);
+    return new JavaObjectTerm(user);
   }
 
   private static IdentifiedUser.GenericFactory userFactory(Prolog engine) {
diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl
index 3313162..5acc831 100644
--- a/gerrit-server/src/main/prolog/gerrit_common.pl
+++ b/gerrit-server/src/main/prolog/gerrit_common.pl
@@ -25,8 +25,7 @@
 %%   predicate that needs to obtain it.
 %%
 init :-
-  define_hash(commit_labels),
-  define_hash(current_user).
+  define_hash(commit_labels).
 
 define_hash(A) :- hash_exists(A), !, hash_clear(A).
 define_hash(A) :- atom(A), !, new_hash(_, [alias(A)]).
@@ -98,6 +97,10 @@
 %%   Lookup the range allowed to be used.
 %%
 user_label_range(Label, Who, Min, Max) :-
+  hash_get(commit_labels, '$fast_range', true), !,
+  atom(Label),
+  assume_range_from_label(Label, Who, Min, Max).
+user_label_range(Label, Who, Min, Max) :-
   Who = user(_), !,
   atom(Label),
   current_user(Who, User),
@@ -106,6 +109,14 @@
   clause(user:test_grant(Label, test_user(Name), range(Min, Max)), _)
   .
 
+assume_range_from_label :-
+  hash_put(commit_labels, '$fast_range', true).
+
+assume_range_from_label(Label, Who, Min, Max) :-
+  commit_label(label(Label, Value), Who), !,
+  Min = Value, Max = Value.
+assume_range_from_label(_, _, 0, 0).
+
 
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 %%
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
index f2f0fc76..1eb6842 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
@@ -31,7 +31,8 @@
 ## The ChangeFooter.vm template will determine the contents of the footer
 ## text that will be appended to ALL emails related to changes.
 ##
---
+#set ($SPACE = " ")
+--$SPACE
 #if ($email.changeUrl)
 To view, visit $email.changeUrl
 #set ($notblank = 1)
diff --git a/gerrit-sshd/.gitignore b/gerrit-sshd/.gitignore
index 194bedc..8deb9bd 100644
--- a/gerrit-sshd/.gitignore
+++ b/gerrit-sshd/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-sshd.iml
\ No newline at end of file
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
index 55e8725..1c197a0 100644
--- a/gerrit-sshd/pom.xml
+++ b/gerrit-sshd/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-sshd</artifactId>
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
index 54b0bb5..558707b 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.sshd.args4j.AccountGroupIdHandler;
 import com.google.gerrit.sshd.args4j.AccountGroupUUIDHandler;
 import com.google.gerrit.sshd.args4j.AccountIdHandler;
+import com.google.gerrit.sshd.args4j.ObjectIdHandler;
 import com.google.gerrit.sshd.args4j.PatchSetIdHandler;
 import com.google.gerrit.sshd.args4j.ProjectControlHandler;
 import com.google.gerrit.sshd.args4j.SocketAddressHandler;
@@ -48,6 +49,7 @@
 import org.apache.sshd.common.KeyPairProvider;
 import org.apache.sshd.server.CommandFactory;
 import org.apache.sshd.server.PublickeyAuthenticator;
+import org.eclipse.jgit.lib.ObjectId;
 import org.kohsuke.args4j.spi.OptionHandler;
 
 import java.net.SocketAddress;
@@ -118,6 +120,7 @@
     registerOptionHandler(Account.Id.class, AccountIdHandler.class);
     registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
     registerOptionHandler(AccountGroup.UUID.class, AccountGroupUUIDHandler.class);
+    registerOptionHandler(ObjectId.class, ObjectIdHandler.class);
     registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
     registerOptionHandler(ProjectControl.class, ProjectControlHandler.class);
     registerOptionHandler(SocketAddress.class, SocketAddressHandler.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ObjectIdHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ObjectIdHandler.java
new file mode 100644
index 0000000..adb5ad6
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ObjectIdHandler.java
@@ -0,0 +1,47 @@
+// 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.sshd.args4j;
+
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+public class ObjectIdHandler extends OptionHandler<ObjectId> {
+
+  @Inject
+  public ObjectIdHandler(@Assisted final CmdLineParser parser,
+      @Assisted final OptionDef option, @Assisted final Setter<ObjectId> setter) {
+    super(parser, option, setter);
+  }
+
+  @Override
+  public int parseArguments(Parameters params) throws CmdLineException {
+    final String n = params.getParameter(0);
+    setter.addValue(ObjectId.fromString(n));
+    return 1;
+  }
+
+  @Override
+  public String getDefaultMetaVariable() {
+    return "COMMIT";
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
new file mode 100644
index 0000000..fd58221
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -0,0 +1,118 @@
+// 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.sshd.commands;
+
+import com.google.gerrit.common.errors.PermissionDeniedException;
+import com.google.gerrit.server.git.BanCommit;
+import com.google.gerrit.server.git.BanCommitResult;
+import com.google.gerrit.server.git.IncompleteUserInfoException;
+import com.google.gerrit.server.git.MergeException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.inject.Inject;
+
+import org.apache.sshd.server.Environment;
+import org.eclipse.jgit.lib.ObjectId;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+public class BanCommitCommand extends BaseCommand {
+
+  @Option(name = "--reason", aliases = {"-r"}, metaVar = "REASON", usage = "reason for banning the commit")
+  private String reason;
+
+  @Argument(index = 0, required = true, metaVar = "PROJECT",
+      usage = "name of the project for which the commit should be banned")
+  private ProjectControl projectControl;
+
+  @Argument(index = 1, required = true, multiValued = true, metaVar = "COMMIT",
+      usage = "commit(s) that should be banned")
+  private List<ObjectId> commitsToBan = new ArrayList<ObjectId>();
+
+  @Inject
+  private BanCommit.Factory banCommitFactory;
+
+  @Override
+  public void start(final Environment env) throws IOException {
+    startThread(new CommandRunnable() {
+      @Override
+      public void run() throws Exception {
+        parseCommandLine();
+        BanCommitCommand.this.display();
+      }
+    });
+  }
+
+  private void display() throws Failure {
+    try {
+      final BanCommitResult result =
+          banCommitFactory.create().ban(projectControl, commitsToBan, reason);
+
+      final PrintWriter stdout = toPrintWriter(out);
+      try {
+        final List<ObjectId> newlyBannedCommits =
+            result.getNewlyBannedCommits();
+        if (!newlyBannedCommits.isEmpty()) {
+          stdout.print("The following commits were banned:\n");
+          printCommits(stdout, newlyBannedCommits);
+        }
+
+        final List<ObjectId> alreadyBannedCommits =
+            result.getAlreadyBannedCommits();
+        if (!alreadyBannedCommits.isEmpty()) {
+          stdout.print("The following commits were already banned:\n");
+          printCommits(stdout, alreadyBannedCommits);
+        }
+
+        final List<ObjectId> ignoredIds = result.getIgnoredObjectIds();
+        if (!ignoredIds.isEmpty()) {
+          stdout.print("The following ids do not represent commits"
+              + " and were ignored:\n");
+          printCommits(stdout, ignoredIds);
+        }
+      } finally {
+        stdout.flush();
+      }
+    } catch (PermissionDeniedException e) {
+      throw die(e);
+    } catch (IOException e) {
+      throw die(e);
+    } catch (IncompleteUserInfoException e) {
+      throw die(e);
+    } catch (MergeException e) {
+      throw die(e);
+    } catch (InterruptedException e) {
+      throw die(e);
+    }
+  }
+
+  private static void printCommits(final PrintWriter stdout,
+      final List<ObjectId> commits) {
+    boolean first = true;
+    for (final ObjectId c : commits) {
+      if (!first) {
+        stdout.print(",\n");
+      }
+      stdout.print(c.getName());
+      first = false;
+    }
+    stdout.print("\n\n");
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index 16461b6..4d7c93e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -35,6 +35,7 @@
     // SlaveCommandModule.
 
     command(gerrit).toProvider(new DispatchCommandProvider(gerrit));
+    command(gerrit, "ban-commit").to(BanCommitCommand.class);
     command(gerrit, "flush-caches").to(FlushCaches.class);
     command(gerrit, "ls-projects").to(ListProjectsCommand.class);
     command(gerrit, "ls-groups").to(ListGroupsCommand.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
index bc4e0bb..d56d1cd 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
@@ -73,7 +73,7 @@
 
   private void schedule() throws Failure {
     if (all && projectNames.size() > 0) {
-      throw new Failure(1, "error: cannot combine --all and PROJECT");
+      throw new UnloggedFailure(1, "error: cannot combine --all and PROJECT");
     }
 
     if (!replication.isEnabled()) {
@@ -89,7 +89,7 @@
         if (projectCache.get(key) != null) {
           replication.scheduleFullSync(key, urlMatch);
         } else {
-          throw new Failure(1, "error: '" + name + "': not a Gerrit project");
+          throw new UnloggedFailure(1, "error: '" + name + "': not a Gerrit project");
         }
       }
     }
diff --git a/gerrit-util-cli/.gitignore b/gerrit-util-cli/.gitignore
index 194bedc..35069e7 100644
--- a/gerrit-util-cli/.gitignore
+++ b/gerrit-util-cli/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-util-cli.iml
\ No newline at end of file
diff --git a/gerrit-util-cli/pom.xml b/gerrit-util-cli/pom.xml
index 4ecbda4..4886d09 100644
--- a/gerrit-util-cli/pom.xml
+++ b/gerrit-util-cli/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-cli</artifactId>
diff --git a/gerrit-util-ssl/.gitignore b/gerrit-util-ssl/.gitignore
index 194bedc..e552ad5 100644
--- a/gerrit-util-ssl/.gitignore
+++ b/gerrit-util-ssl/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-util-ssl.iml
\ No newline at end of file
diff --git a/gerrit-util-ssl/pom.xml b/gerrit-util-ssl/pom.xml
index 2e49d47..beedb8f 100644
--- a/gerrit-util-ssl/pom.xml
+++ b/gerrit-util-ssl/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-ssl</artifactId>
diff --git a/gerrit-war/.gitignore b/gerrit-war/.gitignore
index 194bedc..dc8c7ad 100644
--- a/gerrit-war/.gitignore
+++ b/gerrit-war/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-war.iml
\ No newline at end of file
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index 733d976a..1f3750e 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-war</artifactId>
diff --git a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml
index 117bf61..3ae9440 100644
--- a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml
+++ b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://jetty.eclipse.org/configure.dtd">
+<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
 <!--
 
   Jetty configuration to place "gerrit.war" into the root context,
diff --git a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml
index 652acad..59cc040 100644
--- a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml
+++ b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://jetty.eclipse.org/configure.dtd">
+<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
 <!--
 
   Jetty configuration to correctly handle SSL/HTTPS traffic when
diff --git a/pom.xml b/pom.xml
index 377b212..8c14a87 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-parent</artifactId>
   <packaging>pom</packaging>
-  <version>2.4-SNAPSHOT</version>
+  <version>2.5-SNAPSHOT</version>
 
   <name>Gerrit Code Review - Parent</name>
   <url>http://code.google.com/p/gerrit/</url>