Merge changes I6f8b9302,If538bbc8,I4052ade1,Id07b2b62,I2014a220,I577ae60e,I87e58dda,I9a72cd28,Ic72892ae

* changes:
  REST API /projects/$SUGGEST
  REST API /projects/
  REST API /accounts/self/capabilities
  Define a native REST API client
  /projects/: Support JSON output format
  Make ls-projects available on HTTP as GET /projects/
  Support alias "self" in queries
  Update GWT to 2.4.0
  Update Gson to 2.1
diff --git a/Documentation/cmd-ls-projects.txt b/Documentation/cmd-ls-projects.txt
index 7782aa8..c1b37fa 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -23,7 +23,7 @@
 
 ACCESS
 ------
-Any user who has configured an SSH key.
+Any user who has configured an SSH key, or by an user over HTTP.
 
 SCRIPTING
 ---------
@@ -64,6 +64,15 @@
 `all`:: Any type of project.
 --
 
+--format::
+	What output format to display the results in.
++
+--
+`text`:: Simple text based format.
+`json`:: Map of JSON objects describing each project.
+`json_compact`:: Minimized JSON output.
+--
+
 --all::
 	Display all projects that are accessible by the calling user
 	account. Besides the projects that the calling user account has
@@ -72,12 +81,43 @@
 	the 'READ' access right is not assigned to the calling user
 	account).
 
+--limit::
+	Cap the number of results to the first N matches.
+
+HTTP
+----
+This command is also available over HTTP, as `/projects/` for
+anonymous access and `/a/projects/` for authenticated access.
+Named options are available as query parameters. Results can
+be limited to projects matching a prefix by supplying the prefix
+as part of the URL, for example `/projects/external/` lists only
+projects whose name start with the string `external/`.
+
+Over HTTP the `json_compact` output format is assumed if the client
+explicitly asks for JSON using HTTP header `Accept: application/json`.
+When any JSON output format is used on HTTP, readers must skip the
+first line produced. The first line is a garbage JSON string crafted
+to prevent a browser from executing the response in a script tag.
+
+Output will be gzip compressed if `Accept-Encoding: gzip` was used
+by the client in the request headers.
+
 EXAMPLES
 --------
 
 List visible projects:
 =====
 	$ ssh -p 29418 review.example.com gerrit ls-projects
+	platform/manifest
+	tools/gerrit
+	tools/gwtorm
+
+	$ curl http://review.example.com/projects/
+	platform/manifest
+	tools/gerrit
+	tools/gwtorm
+
+	$ curl http://review.example.com/projects/tools/
 	tools/gerrit
 	tools/gwtorm
 =====
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 5143bf7..540e998 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -18,6 +18,7 @@
 * link:user-signedoffby.html[Signed-off-by Lines]
 * link:access-control.html[Access Controls]
 * link:error-messages.html[Error Messages]
+* link:rest-api.html[REST API]
 * link:user-submodules.html[Subscribing to Git Submodules]
 
 Installation
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
new file mode 100644
index 0000000..f07b9a9
--- /dev/null
+++ b/Documentation/rest-api.txt
@@ -0,0 +1,134 @@
+Gerrit Code Review - REST API
+=============================
+
+Gerrit Code Review comes with a REST like API available over HTTP.
+The API is suitable for automated tools to build upon, as well as
+supporting some ad-hoc scripting use cases.
+
+Protocol Details
+----------------
+
+Authentication
+~~~~~~~~~~~~~~
+By default all REST endpoints assume anonymous access and filter
+results to correspond to what anonymous users can read (which may
+be nothing at all).
+
+Users (and programs) may authenticate using HTTP authentication by
+supplying the HTTP password from the user's account settings page.
+Gerrit by default uses HTTP digest authentication. To authenticate,
+prefix the endpoint URL with `/a/`. For example to authenticate to
+`/projects/` request URL `/a/projects/`.
+
+Output Format
+~~~~~~~~~~~~~
+Most APIs return text format by default. JSON can be requested
+by setting the `Accept` HTTP request header to include
+`application/json`, for example:
+
+----
+  GET /projects/ HTTP/1.0
+  Accept: application/json
+----
+
+JSON responses are encoded using UTF-8 and use content type
+`application/json`. The JSON response body starts with magic prefix
+line that must be stripped before feeding the rest of the response
+body to a JSON parser:
+
+----
+  )]}'
+  [ ... valid JSON ... ]
+----
+
+The default JSON format is `JSON_COMPACT`, which skips unnecessary
+whitespace. This is not the easiest format for a human to read. Many
+examples in this documentation use `format=JSON` as a query parameter
+to obtain pretty formatting in the response. Producing (and parsing)
+the compact format is more efficient, so most tools should prefer the
+default compact format.
+
+Responses will be gzip compressed by the server if the HTTP
+`Accept-Encoding` request header is set to `gzip`. This may
+save on network transfer time for larger responses.
+
+Endpoints
+---------
+
+[[accounts_self_capabilities]]
+/accounts/self/capabilities (Account Capabilities)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Returns the global capabilities (such as createProject or
+createGroup) that are enabled for the calling user. This can be used
+by UI tools to discover if administrative features are available
+to the caller, so they can hide (or show) relevant UI actions.
+
+----
+  GET /accounts/self/capabilities?format=JSON HTTP/1.0
+
+  )]}'
+  {
+    "queryLimit": {
+      "min": 0,
+      "max": 500
+    }
+  }
+----
+
+Administrator that has authenticated with digest authentication:
+----
+  GET /a/accounts/self/capabilities?format=JSON HTTP/1.0
+  Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
+
+  )]}'
+  {
+    "administrateServer": true,
+    "queryLimit": {
+      "min": 0,
+      "max": 500
+    },
+    "createAccount": true,
+    "createGroup": true,
+    "createProject": true,
+    "killTask": true,
+    "viewCaches": true,
+    "flushCaches": true,
+    "viewConnections": true,
+    "viewQueue": true,
+    "startReplication": true
+  }
+----
+
+[[projects]]
+/projects/ (List Projects)
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+Lists the projects accessible by the caller. This is the same as
+using the link:cmd-ls-projects.html[ls-projects] command over SSH,
+and accepts the same options as query parameters.
+
+----
+  GET /projects/?format=JSON&d HTTP/1.0
+
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+   
+  )]}'
+  {
+    "external/bison": {
+      "description": "GNU parser generator"
+    },
+    "external/gcc": {},
+    "external/openssl": {
+      "description": "encryption\ncrypto routines"
+    },
+    "test": {
+      "description": "\u003chtml\u003e is escaped"
+    }
+  }
+----
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 3aafe8c..890c964 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -75,7 +75,8 @@
 [[owner]]
 owner:'USER'::
 +
-Changes originally submitted by 'USER'.
+Changes originally submitted by 'USER'. The special case of
+`owner:self` will find changes owned by the caller.
 
 [[ownerin]]
 ownerin:'GROUP'::
@@ -85,7 +86,9 @@
 [[reviewer]]
 reviewer:'USER'::
 +
-Changes that have been, or need to be, reviewed by 'USER'.
+Changes that have been, or need to be, reviewed by 'USER'. The
+special case of `reviewer:self` will find changes where the caller
+has been added as a reviewer.
 
 [[reviewerin]]
 reviewerin:'GROUP'::
@@ -213,6 +216,16 @@
 True if there is at least one non-zero score on the change, in any
 approval category, by any user.
 
+is:owner::
++
+True on any change where the current user is the change owner.
+Same as `owner:self`.
+
+is:reviewer::
++
+True on any change where the current user is a reviewer.
+Same as `reviewer:self`.
+
 is:open::
 +
 True if the change is other open or submitted, merge pending.
@@ -373,16 +386,20 @@
 starredby:'USER'::
 +
 Matches changes that have been starred by 'USER'.
+The special case `starredby:self` applies to the caller.
 
 watchedby:'USER'::
 +
 Matches changes that 'USER' has configured watch filters for.
+The special case `watchedby:self` applies to the caller.
 
 draftby:'USER'::
 +
 Matches changes that 'USER' has left unpublished drafts on.
 Since the drafts are unpublished, it is not possible to see the
-draft text, or even how many drafts there are.
+draft text, or even how many drafts there are. The special case
+of `draftby:self` will find changes where the caller has created
+a draft comment.
 
 limit:'CNT'::
 +
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
index d3d2a4d..64444b4 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.common.data;
 
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 
 /** Server wide capabilities. Represented as {@link Permission} objects. */
@@ -73,23 +75,34 @@
   /** Can view all pending tasks in the queue (not just the filtered set). */
   public static final String VIEW_QUEUE = "viewQueue";
 
+  private static final List<String> NAMES_ALL;
   private static final List<String> NAMES_LC;
 
   static {
-    NAMES_LC = new ArrayList<String>();
-    NAMES_LC.add(ADMINISTRATE_SERVER.toLowerCase());
-    NAMES_LC.add(CREATE_ACCOUNT.toLowerCase());
-    NAMES_LC.add(CREATE_GROUP.toLowerCase());
-    NAMES_LC.add(CREATE_PROJECT.toLowerCase());
-    NAMES_LC.add(EMAIL_REVIEWERS.toLowerCase());
-    NAMES_LC.add(FLUSH_CACHES.toLowerCase());
-    NAMES_LC.add(KILL_TASK.toLowerCase());
-    NAMES_LC.add(PRIORITY.toLowerCase());
-    NAMES_LC.add(QUERY_LIMIT.toLowerCase());
-    NAMES_LC.add(START_REPLICATION.toLowerCase());
-    NAMES_LC.add(VIEW_CACHES.toLowerCase());
-    NAMES_LC.add(VIEW_CONNECTIONS.toLowerCase());
-    NAMES_LC.add(VIEW_QUEUE.toLowerCase());
+    NAMES_ALL = new ArrayList<String>();
+    NAMES_ALL.add(ADMINISTRATE_SERVER);
+    NAMES_ALL.add(CREATE_ACCOUNT);
+    NAMES_ALL.add(CREATE_GROUP);
+    NAMES_ALL.add(CREATE_PROJECT);
+    NAMES_ALL.add(EMAIL_REVIEWERS);
+    NAMES_ALL.add(FLUSH_CACHES);
+    NAMES_ALL.add(KILL_TASK);
+    NAMES_ALL.add(PRIORITY);
+    NAMES_ALL.add(QUERY_LIMIT);
+    NAMES_ALL.add(START_REPLICATION);
+    NAMES_ALL.add(VIEW_CACHES);
+    NAMES_ALL.add(VIEW_CONNECTIONS);
+    NAMES_ALL.add(VIEW_QUEUE);
+
+    NAMES_LC = new ArrayList<String>(NAMES_ALL.size());
+    for (String name : NAMES_ALL) {
+      NAMES_LC.add(name.toLowerCase());
+    }
+  }
+
+  /** @return all valid capability names. */
+  public static Collection<String> getAllNames() {
+    return Collections.unmodifiableList(NAMES_ALL);
   }
 
   /** @return true if the name is recognized as a capability name. */
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
index 85518b2..d52a724 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
@@ -26,9 +26,6 @@
 
 @RpcImpl(version = Version.V2_0)
 public interface SuggestService extends RemoteJsonService {
-  void suggestProjectNameKey(String query, int limit,
-      AsyncCallback<List<Project.NameKey>> callback);
-
   void suggestAccount(String query, Boolean enabled, int limit,
       AsyncCallback<List<AccountInfo>> callback);
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
index 76ce384..955c8e2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
@@ -56,6 +56,10 @@
 
   @Override
   public void onRpcStart(final RpcStartEvent event) {
+    onRpcStart();
+  }
+
+  public void onRpcStart() {
     if (++activeCalls == 1) {
       if (hideDepth == 0) {
         loading.setVisible(true);
@@ -65,6 +69,10 @@
 
   @Override
   public void onRpcComplete(final RpcCompleteEvent event) {
+    onRpcComplete();
+  }
+
+  public void onRpcComplete() {
     if (--activeCalls == 0) {
       loading.setVisible(false);
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java
new file mode 100644
index 0000000..0565d3e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java
@@ -0,0 +1,36 @@
+// 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.account;
+
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+
+/** Capabilities the caller has from {@code /accounts/self/capabilities}.  */
+public class AccountCapabilities extends JavaScriptObject {
+  public static void all(AsyncCallback<AccountCapabilities> cb, String... filter) {
+    RestApi api = new RestApi("/accounts/self/capabilities");
+    for (String name : filter) {
+      api.addParameter("q", name);
+    }
+    api.send(cb);
+  }
+
+  protected AccountCapabilities() {
+  }
+
+  public final native boolean canPerform(String name)
+  /*-{ return this[name] ? true : false; }-*/;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
index 5a62eac..1334d87 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.HintTextBox;
@@ -22,7 +23,6 @@
 import com.google.gerrit.client.ui.ProjectsTable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccountProjectWatchInfo;
-import com.google.gerrit.common.data.ProjectList;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
@@ -226,14 +226,14 @@
 
         // prevent user input from being overwritten by simply poping up
         if (! popingUp || "".equals(nameBox.getText()) ) {
-          nameBox.setText(getRowItem(row).getName());
+          nameBox.setText(getRowItem(row).name());
         }
       }
 
       @Override
       protected void onOpenRow(final int row) {
         super.onOpenRow(row);
-        nameBox.setText(getRowItem(row).getName());
+        nameBox.setText(getRowItem(row).name());
         doAddNew();
       }
     };
@@ -361,11 +361,10 @@
   }
 
   protected void populateProjects() {
-    Util.PROJECT_SVC.visibleProjects(
-        new GerritCallback<ProjectList>() {
+    ProjectMap.all(new GerritCallback<ProjectMap>() {
       @Override
-      public void onSuccess(final ProjectList result) {
-        projectsTab.display(result.getProjects());
+      public void onSuccess(final ProjectMap result) {
+        projectsTab.display(result);
         if (firstPopupLoad) { // Display was delayed until table was loaded
           firstPopupLoad = false;
           displayPopup();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
index 0ce2d77..43558be 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
@@ -17,6 +17,8 @@
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.projects.ProjectInfo;
+import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.HintTextBox;
 import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
@@ -38,8 +40,6 @@
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtjsonrpc.common.VoidResult;
 
-import java.util.List;
-
 public class CreateProjectScreen extends Screen {
   private NpTextBox project;
   private Button create;
@@ -127,31 +127,30 @@
       }
 
       @Override
-      protected void populate(final int row, final Project k) {
-        final Anchor projectLink = new Anchor(k.getName());
+      protected void populate(final int row, final ProjectInfo k) {
+        final Anchor projectLink = new Anchor(k.name());
         projectLink.addClickHandler(new ClickHandler() {
 
           @Override
           public void onClick(ClickEvent event) {
-            sugestParent.setText(getRowItem(row).getName());
+            sugestParent.setText(getRowItem(row).name());
           }
         });
 
         table.setWidget(row, 1, projectLink);
-        table.setText(row, 2, k.getDescription());
+        table.setText(row, 2, k.description());
 
         setRowItem(row, k);
       }
     };
     suggestedParentsTab.setVisible(false);
 
-    Util.PROJECT_SVC
-        .suggestParentCandidates(new GerritCallback<List<Project>>() {
+    ProjectMap.permissions(new GerritCallback<ProjectMap>() {
           @Override
-          public void onSuccess(List<Project> result) {
-            if (result != null && !result.isEmpty()) {
+          public void onSuccess(ProjectMap list) {
+            if (!list.isEmpty()) {
               suggestedParentsTab.setVisible(true);
-              suggestedParentsTab.display(result);
+              suggestedParentsTab.display(list);
               suggestedParentsTab.finishDisplay();
             }
           }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index 9d62d34..c0a55d0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
@@ -14,8 +14,11 @@
 
 package com.google.gerrit.client.admin;
 
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
+
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountCapabilities;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.AccountScreen;
@@ -48,11 +51,17 @@
   @Override
   protected void onLoad() {
     super.onLoad();
+    addPanel.setVisible(false);
+    AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
+      @Override
+      public void onSuccess(AccountCapabilities ac) {
+        addPanel.setVisible(ac.canPerform(CREATE_GROUP));
+      }
+    }, CREATE_GROUP);
     Util.GROUP_SVC
         .visibleGroups(new ScreenLoadCallback<GroupList>(this) {
           @Override
           protected void preDisplay(GroupList result) {
-            addPanel.setVisible(result.isCanCreateGroup());
             groups.display(result.getGroups());
             groups.finishDisplay();
           }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
index 3599c3c..5cb178c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -14,15 +14,19 @@
 
 package com.google.gerrit.client.admin;
 
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
+
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountCapabilities;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.projects.ProjectInfo;
+import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.ProjectsTable;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ProjectList;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.ui.VerticalPanel;
 
@@ -33,11 +37,17 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.PROJECT_SVC.visibleProjects(new ScreenLoadCallback<ProjectList>(this) {
+    createProjectLinkPanel.setVisible(false);
+    AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
       @Override
-      protected void preDisplay(final ProjectList result) {
-        createProjectLinkPanel.setVisible(result.canCreateProject());
-        projects.display(result.getProjects());
+      public void onSuccess(AccountCapabilities ac) {
+        createProjectLinkPanel.setVisible(ac.canPerform(CREATE_PROJECT));
+      }
+    }, CREATE_PROJECT);
+    ProjectMap.all(new ScreenLoadCallback<ProjectMap>(this) {
+      @Override
+      protected void preDisplay(final ProjectMap result) {
+        projects.display(result);
         projects.finishDisplay();
       }
     });
@@ -61,14 +71,14 @@
         History.newItem(link(getRowItem(row)));
       }
 
-      private String link(final Project item) {
-        return Dispatcher.toProjectAdmin(item.getNameKey(), ProjectScreen.INFO);
+      private String link(final ProjectInfo item) {
+        return Dispatcher.toProjectAdmin(item.name_key(), ProjectScreen.INFO);
       }
 
       @Override
-      protected void populate(final int row, final Project k) {
-        table.setWidget(row, 1, new Hyperlink(k.getName(), link(k)));
-        table.setText(row, 2, k.getDescription());
+      protected void populate(final int row, final ProjectInfo k) {
+        table.setWidget(row, 1, new Hyperlink(k.name(), link(k)));
+        table.setText(row, 2, k.description());
 
         setRowItem(row, k);
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
new file mode 100644
index 0000000..80c1feb
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
@@ -0,0 +1,46 @@
+// 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.projects;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.ui.SuggestOracle;
+
+public class ProjectInfo
+    extends JavaScriptObject
+    implements SuggestOracle.Suggestion {
+  public final Project.NameKey name_key() {
+    return new Project.NameKey(name());
+  }
+
+  public final native String name() /*-{ return this.name; }-*/;
+  public final native String description() /*-{ return this.description; }-*/;
+
+  @Override
+  public final String getDisplayString() {
+    if (description() != null) {
+      return name() + " (" + description() + ")";
+    }
+    return name();
+  }
+
+  @Override
+  public final String getReplacementString() {
+    return name();
+  }
+
+  protected ProjectInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
new file mode 100644
index 0000000..55bb902
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
@@ -0,0 +1,50 @@
+// 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.projects;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.http.client.URL;
+
+/** Projects available from {@code /projects/}. */
+public class ProjectMap extends NativeMap<ProjectInfo> {
+  public static void all(AsyncCallback<ProjectMap> callback) {
+    new RestApi("/projects/")
+        .addParameterRaw("type", "ALL")
+        .addParameterTrue("all")
+        .addParameterTrue("d") // description
+        .send(NativeMap.copyKeysIntoChildren(callback));
+  }
+
+  public static void permissions(AsyncCallback<ProjectMap> callback) {
+    new RestApi("/projects/")
+        .addParameterRaw("type", "PERMISSIONS")
+        .addParameterTrue("all")
+        .addParameterTrue("d") // description
+        .send(NativeMap.copyKeysIntoChildren(callback));
+  }
+
+  public static void suggest(String prefix, int limit, AsyncCallback<ProjectMap> cb) {
+    new RestApi("/projects/" + URL.encode(prefix).replaceAll("[?]", "%3F"))
+        .addParameterRaw("type", "ALL")
+        .addParameter("n", limit)
+        .addParameterTrue("d") // description
+        .send(NativeMap.copyKeysIntoChildren(cb));
+  }
+
+  protected ProjectMap() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java
new file mode 100644
index 0000000..e820fe0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java
@@ -0,0 +1,55 @@
+// 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.rpc;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+import java.util.AbstractList;
+import java.util.List;
+
+/** A read-only list of native JavaScript objects stored in a JSON array. */
+public class NativeList<T extends JavaScriptObject> extends JavaScriptObject {
+  protected NativeList() {
+  }
+
+  public final List<T> asList() {
+    return new AbstractList<T>() {
+      @Override
+      public T set(int index, T element) {
+        T old = NativeList.this.get(index);
+        NativeList.this.set0(index, element);
+        return old;
+      }
+
+      @Override
+      public T get(int index) {
+        return NativeList.this.get(index);
+      }
+
+      @Override
+      public int size() {
+        return NativeList.this.size();
+      }
+    };
+  }
+
+  public final boolean isEmpty() {
+    return size() == 0;
+  }
+
+  public final native int size() /*-{ return this.length; }-*/;
+  public final native T get(int i) /*-{ return this[i]; }-*/;
+  private final native void set0(int i, T v) /*-{ this[i] = v; }-*/;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
new file mode 100644
index 0000000..cde9041
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
@@ -0,0 +1,93 @@
+// 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.rpc;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+
+import java.util.Set;
+
+/** A map of native JSON objects, keyed by a string. */
+public class NativeMap<T extends JavaScriptObject> extends JavaScriptObject {
+  /**
+   * Loop through the result map's entries and copy the key strings into the
+   * "name" property of the corresponding child object. This only runs on the
+   * top level map of the result, and requires the children to be JSON objects
+   * and not a JSON primitive (e.g. boolean or string).
+   */
+  public static <T extends JavaScriptObject,
+      M extends NativeMap<T>> AsyncCallback<M> copyKeysIntoChildren(
+      AsyncCallback<M> callback) {
+    return copyKeysIntoChildren("name", callback);
+  }
+
+  /** Loop through the result map and set asProperty on the children. */
+  public static <T extends JavaScriptObject,
+      M extends NativeMap<T>> AsyncCallback<M> copyKeysIntoChildren(
+      final String asProperty, AsyncCallback<M> callback) {
+    return new TransformCallback<M, M>(callback) {
+      @Override
+      protected M transform(M result) {
+        result.copyKeysIntoChildren(asProperty);
+        return result;
+      }
+    };
+  }
+
+  protected NativeMap() {
+  }
+
+  public final Set<String> keySet() {
+    return Natives.keys(this);
+  }
+
+  public final native NativeList<T> values()
+  /*-{
+    var s = this;
+    var v = [];
+    var i = 0;
+    for (var k in s) {
+      if (s.hasOwnProperty(k)) {
+        v[i++] = s[k];
+      }
+    }
+    return v;
+  }-*/;
+
+  public final int size() {
+    return keySet().size();
+  }
+
+  public final boolean isEmpty() {
+    return size() == 0;
+  }
+
+  public final boolean containsKey(String n) {
+    return get(n) != null;
+  }
+
+  public final native T get(String n) /*-{ return this[n]; }-*/;
+
+  public final native void copyKeysIntoChildren(String p)
+  /*-{
+    var s = this;
+    for (var k in s) {
+      if (s.hasOwnProperty(k)) {
+        var c = s[k];
+        c[p] = k;
+      }
+    }
+  }-*/;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java
new file mode 100644
index 0000000..3d99c9e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java
@@ -0,0 +1,56 @@
+// 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.rpc;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.json.client.JSONObject;
+
+import java.util.Collections;
+import java.util.Set;
+
+public class Natives {
+  /**
+   * Get the names of defined properties on the object. The returned set
+   * iterates in the native iteration order, which may match the source order.
+   */
+  public static Set<String> keys(JavaScriptObject obj) {
+    if (obj != null) {
+      return new JSONObject(obj).keySet();
+    }
+    return Collections.emptySet();
+  }
+
+  public static <T extends JavaScriptObject> T parseJSON(String json) {
+    if (parser == null) {
+      parser = bestJsonParser();
+    }
+    return parse0(parser, json);
+  }
+
+  private static native <T extends JavaScriptObject>
+  T parse0(JavaScriptObject p, String s)
+  /*-{ return p(s); }-*/;
+
+  private static JavaScriptObject parser;
+  private static native JavaScriptObject bestJsonParser()
+  /*-{
+    if ($wnd.JSON && typeof $wnd.JSON.parse === 'function')
+      return $wnd.JSON.parse;
+    return function(s) { return eval('(' + s + ')'); };
+  }-*/;
+
+  private Natives() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
new file mode 100644
index 0000000..bd69092
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
@@ -0,0 +1,174 @@
+// 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.rpc;
+
+import com.google.gerrit.client.RpcStatus;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestBuilder;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.RequestException;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.http.client.URL;
+import com.google.gwt.user.client.rpc.StatusCodeException;
+import com.google.gwtjsonrpc.client.RemoteJsonException;
+import com.google.gwtjsonrpc.client.ServerUnavailableException;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.JsonConstants;
+
+/** Makes a REST API call to the server. */
+public class RestApi {
+  /**
+   * Expected JSON content body prefix that prevents XSSI.
+   * <p>
+   * The server always includes this line as the first line of the response
+   * content body when the response body is formatted as JSON. It gets inserted
+   * by the server to prevent the resource from being imported into another
+   * domain's page using a &lt;script&gt; tag. This line must be removed before
+   * the JSON can be parsed.
+   */
+  private static final String JSON_MAGIC = ")]}'\n";
+
+  private StringBuilder url;
+  private boolean hasQueryParams;
+
+  /**
+   * Initialize a new API call.
+   * <p>
+   * By default the JSON format will be selected by including an HTTP Accept
+   * header in the request.
+   *
+   * @param name URL of the REST resource to access, e.g. {@code "/projects/"}
+   *        to list accessible projects from the server.
+   */
+  public RestApi(String name) {
+    if (name.startsWith("/")) {
+      name = name.substring(1);
+    }
+
+    url = new StringBuilder();
+    url.append(GWT.getHostPageBaseURL());
+    url.append(name);
+  }
+
+  public RestApi addParameter(String name, String value) {
+    return addParameterRaw(name, URL.encodeQueryString(value));
+  }
+
+  public RestApi addParameterTrue(String name) {
+    return addParameterRaw(name, null);
+  }
+
+  public RestApi addParameter(String name, boolean value) {
+    return addParameterRaw(name, value ? "t" : "f");
+  }
+
+  public RestApi addParameter(String name, int value) {
+    return addParameterRaw(name, String.valueOf(value));
+  }
+
+  public RestApi addParameterRaw(String name, String value) {
+    if (hasQueryParams) {
+      url.append("&");
+    } else {
+      url.append("?");
+      hasQueryParams = true;
+    }
+    url.append(name);
+    if (value != null) {
+      url.append("=").append(value);
+    }
+    return this;
+  }
+
+  public <T extends JavaScriptObject> void send(final AsyncCallback<T> cb) {
+    RequestBuilder req = new RequestBuilder(RequestBuilder.GET, url.toString());
+    req.setHeader("Accept", JsonConstants.JSON_TYPE);
+    req.setCallback(new RequestCallback() {
+      @Override
+      public void onResponseReceived(Request req, Response res) {
+        RpcStatus.INSTANCE.onRpcComplete();
+        int status = res.getStatusCode();
+        if (status != 200) {
+          if ((400 <= status && status < 500) && isTextBody(res)) {
+            cb.onFailure(new RemoteJsonException(res.getText(), status, null));
+          } else {
+            cb.onFailure(new StatusCodeException(status, res.getStatusText()));
+          }
+          return;
+        }
+
+        if (!isJsonBody(res)) {
+          cb.onFailure(new RemoteJsonException("Invalid JSON"));
+          return;
+        }
+
+        String json = res.getText();
+        if (!json.startsWith(JSON_MAGIC)) {
+          cb.onFailure(new RemoteJsonException("Invalid JSON"));
+          return;
+        }
+
+        T data;
+        try {
+          data = Natives.parseJSON(json.substring(JSON_MAGIC.length()));
+        } catch (RuntimeException e) {
+          cb.onFailure(new RemoteJsonException("Invalid JSON"));
+          return;
+        }
+
+        cb.onSuccess(data);
+      }
+
+      @Override
+      public void onError(Request req, Throwable err) {
+        RpcStatus.INSTANCE.onRpcComplete();
+        if (err.getMessage().contains("XmlHttpRequest.status")) {
+          cb.onFailure(new ServerUnavailableException());
+        } else {
+          cb.onFailure(err);
+        }
+      }
+    });
+    try {
+      RpcStatus.INSTANCE.onRpcStart();
+      req.send();
+    } catch (RequestException e) {
+      RpcStatus.INSTANCE.onRpcComplete();
+      cb.onFailure(e);
+    }
+  }
+
+  private static boolean isJsonBody(Response res) {
+    return isContentType(res, JsonConstants.JSON_TYPE);
+  }
+
+  private static boolean isTextBody(Response res) {
+    return isContentType(res, "text/plain");
+  }
+
+  private static boolean isContentType(Response res, String want) {
+    String type = res.getHeader("Content-Type");
+    if (type == null) {
+      return false;
+    }
+    int semi = type.indexOf(';');
+    if (semi >= 0) {
+      type = type.substring(0, semi).trim();
+    }
+    return want.equals(type);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
new file mode 100644
index 0000000..2cd22cb
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
@@ -0,0 +1,38 @@
+// 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.rpc;
+
+import com.google.gwtjsonrpc.common.AsyncCallback;
+
+/** Transforms a value and passes it on to another callback. */
+public abstract class TransformCallback<I, O> implements AsyncCallback<I>{
+  private final AsyncCallback<O> callback;
+
+  protected TransformCallback(AsyncCallback<O> callback) {
+    this.callback = callback;
+  }
+
+  @Override
+  public void onSuccess(I result) {
+    callback.onSuccess(transform(result));
+  }
+
+  @Override
+  public void onFailure(Throwable caught) {
+    callback.onFailure(caught);
+  }
+
+  protected abstract O transform(I result);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
index be82eff..25ed258 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
@@ -15,49 +15,25 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.RpcStatus;
+import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.user.client.ui.SuggestOracle;
 import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
 
-import java.util.ArrayList;
-import java.util.List;
-
 /** Suggestion Oracle for Project.NameKey entities. */
 public class ProjectNameSuggestOracle extends HighlightSuggestOracle {
   @Override
   public void onRequestSuggestions(final Request req, final Callback callback) {
     RpcStatus.hide(new Runnable() {
+      @Override
       public void run() {
-        SuggestUtil.SVC.suggestProjectNameKey(req.getQuery(), req.getLimit(),
-            new GerritCallback<List<Project.NameKey>>() {
-              public void onSuccess(final List<Project.NameKey> result) {
-                final ArrayList<ProjectNameSuggestion> r =
-                    new ArrayList<ProjectNameSuggestion>(result.size());
-                for (final Project.NameKey p : result) {
-                  r.add(new ProjectNameSuggestion(p));
-                }
-                callback.onSuggestionsReady(req, new Response(r));
+        ProjectMap.suggest(req.getQuery(), req.getLimit(),
+            new GerritCallback<ProjectMap>() {
+              @Override
+              public void onSuccess(ProjectMap map) {
+                callback.onSuggestionsReady(req, new Response(map.values().asList()));
               }
             });
       }
     });
   }
-
-  private static class ProjectNameSuggestion implements
-      SuggestOracle.Suggestion {
-    private final Project.NameKey key;
-
-    ProjectNameSuggestion(final Project.NameKey k) {
-      key = k;
-    }
-
-    public String getDisplayString() {
-      return key.get();
-    }
-
-    public String getReplacementString() {
-      return key.get();
-    }
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
index b768643..50dab9f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
@@ -15,16 +15,19 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.client.projects.ProjectInfo;
+import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 
-public class ProjectsTable extends NavigationTable<Project> {
+public class ProjectsTable extends NavigationTable<ProjectInfo> {
 
   public ProjectsTable() {
     keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.projectListPrev()));
@@ -41,6 +44,7 @@
     fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
   }
 
+  @Override
   protected MyFlexTable createFlexTable() {
     MyFlexTable table = new MyFlexTable() {
       @Override
@@ -78,8 +82,8 @@
   }
 
   @Override
-  protected Object getRowItemKey(final Project item) {
-    return item.getNameKey();
+  protected Object getRowItemKey(final ProjectInfo item) {
+    return item.name();
   }
 
   @Override
@@ -89,17 +93,24 @@
     }
   }
 
-  public void display(final List<Project> projects) {
+  public void display(ProjectMap projects) {
     while (1 < table.getRowCount())
       table.removeRow(table.getRowCount() - 1);
 
-    for (final Project k : projects)
-      insert(table.getRowCount(), k);
+    List<ProjectInfo> list = projects.values().asList();
+    Collections.sort(list, new Comparator<ProjectInfo>() {
+      @Override
+      public int compare(ProjectInfo a, ProjectInfo b) {
+        return a.name().compareTo(b.name());
+      }
+    });
+    for(ProjectInfo p : list)
+      insert(table.getRowCount(), p);
 
     finishDisplay();
   }
 
-  protected void insert(final int row, final Project k) {
+  protected void insert(final int row, final ProjectInfo k) {
     table.insertRow(row);
 
     applyDataRowStyle(row);
@@ -112,9 +123,9 @@
     populate(row, k);
   }
 
-  protected void populate(final int row, final Project k) {
-    table.setText(row, 1, k.getName());
-    table.setText(row, 2, k.getDescription());
+  protected void populate(final int row, final ProjectInfo k) {
+    table.setText(row, 1, k.name());
+    table.setText(row, 2, k.description());
 
     setRowItem(row, k);
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
index c0e3f42..29b5d95 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
@@ -20,12 +20,10 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwtjsonrpc.server.XsrfException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.http.server.GitSmartHttpTools;
 import org.eclipse.jgit.lib.Config;
 
 import java.io.IOException;
@@ -39,7 +37,6 @@
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpServletResponseWrapper;
 
 /**
  * Trust the authentication which is done by the container.
@@ -62,7 +59,7 @@
 
   @Inject
   ContainerAuthFilter(Provider<WebSession> session, AccountCache accountCache,
-      @GerritServerConfig Config config) throws XsrfException {
+      @GerritServerConfig Config config) {
     this.session = session;
     this.accountCache = accountCache;
     this.config = config;
@@ -80,20 +77,14 @@
   public void doFilter(ServletRequest request, ServletResponse response,
       FilterChain chain) throws IOException, ServletException {
     HttpServletRequest req = (HttpServletRequest) request;
-    if (!GitSmartHttpTools.isGitClient(req)) {
-      chain.doFilter(request, response);
-      return;
-    }
-
-    HttpServletResponseWrapper rsp =
-        new HttpServletResponseWrapper((HttpServletResponse) response);
+    HttpServletResponse rsp = (HttpServletResponse) response;
 
     if (verify(req, rsp)) {
       chain.doFilter(req, response);
     }
   }
 
-  private boolean verify(HttpServletRequest req, HttpServletResponseWrapper rsp)
+  private boolean verify(HttpServletRequest req, HttpServletResponse rsp)
       throws IOException {
     String username = req.getRemoteUser();
     if (username == null) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
index 6fd94c9..aa004e3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
@@ -41,5 +41,7 @@
     String git = GitOverHttpServlet.URL_REGEX;
     filterRegex(git).through(authFilter);
     serveRegex(git).with(GitOverHttpServlet.class);
+
+    filter("/a/*").through(authFilter);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
index 49c545d..9e12e8c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
@@ -30,7 +30,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.http.server.GitSmartHttpTools;
 import org.eclipse.jgit.lib.Config;
 
 import java.io.IOException;
@@ -100,11 +99,6 @@
   public void doFilter(ServletRequest request, ServletResponse response,
       FilterChain chain) throws IOException, ServletException {
     HttpServletRequest req = (HttpServletRequest) request;
-    if (!GitSmartHttpTools.isGitClient(req)) {
-      chain.doFilter(request, response);
-      return;
-    }
-
     Response rsp = new Response(req, (HttpServletResponse) response);
 
     if (verify(req, rsp)) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java
new file mode 100644
index 0000000..499c2a5
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java
@@ -0,0 +1,62 @@
+// 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;
+
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+
+/** Requires the user to be authenticated over HTTP. */
+@Singleton
+class RequireIdentifiedUserFilter implements Filter {
+  private final Provider<CurrentUser> user;
+
+  @Inject
+  RequireIdentifiedUserFilter(Provider<CurrentUser> user) {
+    this.user = user;
+  }
+
+  @Override
+  public void init(FilterConfig filterConfig) {
+  }
+
+  @Override
+  public void destroy() {
+  }
+
+  @Override
+  public void doFilter(ServletRequest request,
+      ServletResponse response, FilterChain chain)
+      throws IOException, ServletException {
+    if (user.get() instanceof IdentifiedUser) {
+      chain.doFilter(request, response);
+    } else {
+      HttpServletResponse res = (HttpServletResponse) response;
+      res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java
new file mode 100644
index 0000000..8105e25
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java
@@ -0,0 +1,178 @@
+// 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;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.util.cli.CmdLineParser;
+import com.google.gwtjsonrpc.server.RPCServletUtils;
+import com.google.gwtjsonrpc.common.JsonConstants;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.CmdLineException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public abstract class RestApiServlet extends HttpServlet {
+  private static final long serialVersionUID = 1L;
+  private static final Logger log =
+      LoggerFactory.getLogger(RestApiServlet.class);
+
+  /** MIME type used for a JSON response body. */
+  protected static final String JSON_TYPE = JsonConstants.JSON_TYPE;
+
+  /**
+   * Garbage prefix inserted before JSON output to prevent XSSI.
+   * <p>
+   * This prefix is ")]}'\n" and is designed to prevent a web browser from
+   * executing the response body if the resource URI were to be referenced using
+   * a &lt;script src="...&gt; HTML tag from another web site. Clients using the
+   * HTTP interface will need to always strip the first line of response data to
+   * remove this magic header.
+   */
+  protected static final byte[] JSON_MAGIC;
+
+  static {
+    try {
+      JSON_MAGIC = ")]}'\n".getBytes("UTF-8");
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException("UTF-8 not supported", e);
+    }
+  }
+
+  @Override
+  protected void service(HttpServletRequest req, HttpServletResponse res)
+      throws ServletException, IOException {
+    noCache(res);
+    try {
+      super.service(req, res);
+    } catch (Error err) {
+      handleError(err, req, res);
+    } catch (RuntimeException err) {
+      handleError(err, req, res);
+    }
+  }
+
+  private static void noCache(HttpServletResponse res) {
+    res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
+    res.setHeader("Pragma", "no-cache");
+    res.setHeader("Cache-Control", "no-cache, must-revalidate");
+    res.setHeader("Content-Disposition", "attachment");
+  }
+
+  private static void handleError(
+      Throwable err, HttpServletRequest req, HttpServletResponse res)
+      throws IOException {
+    String uri = req.getRequestURI();
+    if (!Strings.isNullOrEmpty(req.getQueryString())) {
+      uri += "?" + req.getQueryString();
+    }
+    log.error(String.format("Error in %s %s", req.getMethod(), uri), err);
+
+    if (!res.isCommitted()) {
+      res.reset();
+      res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+      noCache(res);
+      sendText(req, res, "Internal Server Error");
+    }
+  }
+
+  protected static boolean acceptsJson(HttpServletRequest req) {
+    String accept = req.getHeader("Accept");
+    if (accept == null) {
+      return false;
+    } else if (JSON_TYPE.equals(accept)) {
+      return true;
+    } else if (accept.startsWith(JSON_TYPE + ",")) {
+      return true;
+    }
+    for (String p : accept.split("[ ,;][ ,;]*")) {
+      if (JSON_TYPE.equals(p)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  protected static void sendText(HttpServletRequest req,
+      HttpServletResponse res, String data) throws IOException {
+    res.setContentType("text/plain");
+    res.setCharacterEncoding("UTF-8");
+    send(req, res, data.getBytes("UTF-8"));
+  }
+
+  protected static void send(HttpServletRequest req, HttpServletResponse res,
+      byte[] data) throws IOException {
+    if (data.length > 256 && RPCServletUtils.acceptsGzipEncoding(req)) {
+      res.setHeader("Content-Encoding", "gzip");
+      data = HtmlDomUtil.compress(data);
+    }
+    res.setContentLength(data.length);
+    OutputStream out = res.getOutputStream();
+    try {
+      out.write(data);
+    } finally {
+      out.close();
+    }
+  }
+
+  public static class ParameterParser {
+    private final CmdLineParser.Factory parserFactory;
+
+    @Inject
+    ParameterParser(CmdLineParser.Factory pf) {
+      this.parserFactory = pf;
+    }
+
+    public <T> boolean parse(T param, HttpServletRequest req,
+        HttpServletResponse res) throws IOException {
+      CmdLineParser clp = parserFactory.create(param);
+      try {
+        @SuppressWarnings("unchecked")
+        Map<String, String[]> parameterMap = req.getParameterMap();
+        clp.parseOptionMap(parameterMap);
+      } catch (CmdLineException e) {
+        if (!clp.wasHelpRequestedByOption()) {
+          res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+          sendText(req, res, e.getMessage());
+          return false;
+        }
+      }
+
+      if (clp.wasHelpRequestedByOption()) {
+        StringWriter msg = new StringWriter();
+        clp.printQueryStringUsage(req.getRequestURI(), msg);
+        msg.write('\n');
+        msg.write('\n');
+        clp.printUsage(msg, null);
+        msg.write('\n');
+        sendText(req, res, msg.toString());
+        return false;
+      }
+
+      return true;
+    }
+  }
+}
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 f90c20d..c299a71 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
@@ -23,6 +23,8 @@
 import com.google.gerrit.httpd.raw.SshInfoServlet;
 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.project.ListProjectsServlet;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwtexpui.server.CacheControlFilter;
@@ -69,6 +71,10 @@
     serveRegex("^/([1-9][0-9]*)/?$").with(directChangeById());
     serveRegex("^/p/(.*)$").with(queryProjectNew());
     serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
+
+    filter("/a/*").through(RequireIdentifiedUserFilter.class);
+    serveRegex("^/(?:a/)?accounts/self/capabilities$").with(AccountCapabilitiesServlet.class);
+    serveRegex("^/(?:a/)?projects/(.*)?$").with(ListProjectsServlet.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 8ee2c41..53dee84 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
@@ -38,6 +38,7 @@
 import com.google.gerrit.server.contact.ContactStoreProvider;
 import com.google.gerrit.server.util.GuiceRequestScopePropagator;
 import com.google.gerrit.server.util.RequestScopePropagator;
+import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -135,6 +136,7 @@
     bind(ChangeUserName.CurrentUser.class);
     factory(ChangeUserName.Factory.class);
     factory(ClearPassword.Factory.class);
+    factory(CmdLineParser.Factory.class);
     factory(GeneratePassword.Factory.class);
 
     bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
index f3e8e65e..d687463 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestServiceImpl.java
@@ -39,8 +39,6 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -60,8 +58,6 @@
   private static final String MAX_SUFFIX = "\u9fa5";
 
   private final Provider<ReviewDb> reviewDbProvider;
-  private final ProjectControl.Factory projectControlFactory;
-  private final ProjectCache projectCache;
   private final AccountCache accountCache;
   private final GroupControl.Factory groupControlFactory;
   private final GroupMembers.Factory groupMembersFactory;
@@ -74,8 +70,7 @@
 
   @Inject
   SuggestServiceImpl(final Provider<ReviewDb> schema,
-      final ProjectControl.Factory projectControlFactory,
-      final ProjectCache projectCache, final AccountCache accountCache,
+      final AccountCache accountCache,
       final GroupControl.Factory groupControlFactory,
       final GroupMembers.Factory groupMembersFactory,
       final Provider<CurrentUser> currentUser,
@@ -85,8 +80,6 @@
       @GerritServerConfig final Config cfg, final GroupCache groupCache) {
     super(schema, currentUser);
     this.reviewDbProvider = schema;
-    this.projectControlFactory = projectControlFactory;
-    this.projectCache = projectCache;
     this.accountCache = accountCache;
     this.groupControlFactory = groupControlFactory;
     this.groupMembersFactory = groupMembersFactory;
@@ -111,28 +104,6 @@
     }
   }
 
-  public void suggestProjectNameKey(final String query, final int limit,
-      final AsyncCallback<List<Project.NameKey>> callback) {
-    final int max = 10;
-    final int n = limit <= 0 ? max : Math.min(limit, max);
-
-    final List<Project.NameKey> r = new ArrayList<Project.NameKey>(n);
-    for (final Project.NameKey nameKey : projectCache.byName(query)) {
-      final ProjectControl ctl;
-      try {
-        ctl = projectControlFactory.validateFor(nameKey);
-      } catch (NoSuchProjectException e) {
-        continue;
-      }
-
-      r.add(ctl.getProject().getNameKey());
-      if (r.size() == n) {
-        break;
-      }
-    }
-    callback.onSuccess(r);
-  }
-
   private interface VisibilityControl {
     boolean isVisible(Account account) throws OrmException;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java
new file mode 100644
index 0000000..0d0ffe7
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java
@@ -0,0 +1,188 @@
+// 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.account;
+
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_ACCOUNT;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
+import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
+import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
+import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
+import static com.google.gerrit.common.data.GlobalCapability.START_REPLICATION;
+import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
+import static com.google.gerrit.common.data.GlobalCapability.VIEW_CONNECTIONS;
+import static com.google.gerrit.common.data.GlobalCapability.VIEW_QUEUE;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.httpd.RestApiServlet;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.git.QueueProvider;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.kohsuke.args4j.Option;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class AccountCapabilitiesServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+  private final ParameterParser paramParser;
+  private final Provider<Impl> factory;
+
+  @Inject
+  AccountCapabilitiesServlet(
+      ParameterParser paramParser, Provider<Impl> factory) {
+    this.paramParser = paramParser;
+    this.factory = factory;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse res)
+      throws IOException {
+    Impl impl = factory.get();
+    if (acceptsJson(req)) {
+      impl.format = OutputFormat.JSON_COMPACT;
+    }
+    if (paramParser.parse(impl, req, res)) {
+      impl.compute();
+
+      ByteArrayOutputStream buf = new ByteArrayOutputStream();
+      OutputStreamWriter out = new OutputStreamWriter(buf, "UTF-8");
+      if (impl.format.isJson()) {
+        res.setContentType(JSON_TYPE);
+        buf.write(JSON_MAGIC);
+        impl.format.newGson().toJson(
+            impl.have,
+            new TypeToken<Map<String, Object>>() {}.getType(),
+            out);
+        out.flush();
+        buf.write('\n');
+      } else {
+        res.setContentType("text/plain");
+        for (Map.Entry<String, Object> e : impl.have.entrySet()) {
+          out.write(e.getKey());
+          if (!(e.getValue() instanceof Boolean)) {
+            out.write(": ");
+            out.write(e.getValue().toString());
+          }
+          out.write('\n');
+        }
+        out.flush();
+      }
+      res.setCharacterEncoding("UTF-8");
+      send(req, res, buf.toByteArray());
+    }
+  }
+
+  static class Impl {
+    private final CapabilityControl cc;
+    private final Map<String, Object> have;
+
+    @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+    private OutputFormat format = OutputFormat.TEXT;
+
+    @Option(name = "-q", metaVar = "CAP", multiValued = true, usage = "Capability to inspect")
+    void addQuery(String name) {
+      if (query == null) {
+        query = Sets.newHashSet();
+      }
+      query.add(name.toLowerCase());
+    }
+    private Set<String> query;
+
+    @Inject
+    Impl(CurrentUser user) {
+      cc = user.getCapabilities();
+      have = Maps.newLinkedHashMap();
+    }
+
+    void compute() {
+      for (String name : GlobalCapability.getAllNames()) {
+        if (!name.equals(PRIORITY) && want(name) && cc.canPerform(name)) {
+          if (GlobalCapability.hasRange(name)) {
+            have.put(name, new Range(cc.getRange(name)));
+          } else {
+            have.put(name, true);
+          }
+        }
+      }
+
+      have.put(CREATE_ACCOUNT, cc.canCreateAccount());
+      have.put(CREATE_GROUP, cc.canCreateGroup());
+      have.put(CREATE_PROJECT, cc.canCreateProject());
+      have.put(KILL_TASK, cc.canKillTask());
+      have.put(VIEW_CACHES, cc.canViewCaches());
+      have.put(FLUSH_CACHES, cc.canFlushCaches());
+      have.put(VIEW_CONNECTIONS, cc.canViewConnections());
+      have.put(VIEW_QUEUE, cc.canViewQueue());
+      have.put(START_REPLICATION, cc.canStartReplication());
+
+      QueueProvider.QueueType queue = cc.getQueueType();
+      if (queue != QueueProvider.QueueType.INTERACTIVE
+          || (query != null && query.contains(PRIORITY))) {
+        have.put(PRIORITY, queue);
+      }
+
+      Iterator<Map.Entry<String, Object>> itr = have.entrySet().iterator();
+      while (itr.hasNext()) {
+        Map.Entry<String, Object> e = itr.next();
+        if (!want(e.getKey())) {
+          itr.remove();
+        } else if (e.getValue() instanceof Boolean && !((Boolean) e.getValue())) {
+          itr.remove();
+        }
+      }
+    }
+
+    private boolean want(String name) {
+      return query == null || query.contains(name.toLowerCase());
+    }
+  }
+
+  private static class Range {
+    private transient PermissionRange range;
+    @SuppressWarnings("unused")
+    private int min;
+    @SuppressWarnings("unused")
+    private int max;
+
+    Range(PermissionRange r) {
+      range = r;
+      min = r.getMin();
+      max = r.getMax();
+    }
+
+    @Override
+    public String toString() {
+      return range.toString();
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java
new file mode 100644
index 0000000..2757640
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java
@@ -0,0 +1,67 @@
+// 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.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.httpd.RestApiServlet;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.project.ListProjects;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URLDecoder;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class ListProjectsServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+  private final ParameterParser paramParser;
+  private final Provider<ListProjects> factory;
+
+  @Inject
+  ListProjectsServlet(ParameterParser paramParser, Provider<ListProjects> ls) {
+    this.paramParser = paramParser;
+    this.factory = ls;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse res)
+      throws IOException {
+    ListProjects impl = factory.get();
+    if (!Strings.isNullOrEmpty(req.getPathInfo())) {
+      impl.setMatchPrefix(URLDecoder.decode(req.getPathInfo(), "UTF-8"));
+    }
+    if (acceptsJson(req)) {
+      impl.setFormat(OutputFormat.JSON_COMPACT);
+    }
+    if (paramParser.parse(impl, req, res)) {
+      ByteArrayOutputStream buf = new ByteArrayOutputStream();
+      if (impl.getFormat().isJson()) {
+        res.setContentType(JSON_TYPE);
+        buf.write(JSON_MAGIC);
+      } else {
+        res.setContentType("text/plain");
+      }
+      impl.display(buf);
+      res.setCharacterEncoding("UTF-8");
+      send(req, res, buf.toByteArray());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/OutputFormat.java b/gerrit-server/src/main/java/com/google/gerrit/server/OutputFormat.java
new file mode 100644
index 0000000..7e1ec4b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/OutputFormat.java
@@ -0,0 +1,71 @@
+// 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;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gwtjsonrpc.server.SqlTimestampDeserializer;
+
+import java.sql.Timestamp;
+
+/** Standard output format used by an API call. */
+public enum OutputFormat {
+  /**
+   * The output is a human readable text format. It may also be regular enough
+   * to be machine readable. Whether or not the text format is machine readable
+   * and will be committed to as a long term format that tools can build upon is
+   * specific to each API call.
+   */
+  TEXT,
+
+  /**
+   * Pretty-printed JSON format. This format uses whitespace to make the output
+   * readable by a human, but is also machine readable with a JSON library. The
+   * structure of the output is a long term format that tools can rely upon.
+   */
+  JSON,
+
+  /**
+   * Same as {@link #JSON}, but with unnecessary whitespace removed to save
+   * generation time and copy costs. Typically JSON_COMPACT format is used by a
+   * browser based HTML client running over the network.
+   */
+  JSON_COMPACT;
+
+  /** @return true when the format is either JSON or JSON_COMPACT. */
+  public boolean isJson() {
+    return this == JSON_COMPACT || this == JSON;
+  }
+
+  /** @return a new Gson instance configured according to the format. */
+  public GsonBuilder newGsonBuilder() {
+    if (!isJson()) {
+      throw new IllegalStateException(String.format("%s is not JSON", this));
+    }
+    GsonBuilder gb = new GsonBuilder()
+      .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+      .registerTypeAdapter(Timestamp.class, new SqlTimestampDeserializer());
+    if (this == OutputFormat.JSON) {
+      gb.setPrettyPrinting();
+    }
+    return gb;
+  }
+
+  /** @return a new Gson instance configured according to the format. */
+  public Gson newGson() {
+    return newGsonBuilder().create();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index 95a8b77..716a5a8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -14,10 +14,14 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.collect.Maps;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.util.TreeFormatter;
+import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -36,6 +40,7 @@
 import java.io.UnsupportedEncodingException;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Map;
 import java.util.SortedSet;
 import java.util.TreeMap;
 import java.util.TreeSet;
@@ -75,6 +80,9 @@
   private final GitRepositoryManager repoManager;
   private final ProjectNode.Factory projectNodeFactory;
 
+  @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+  private OutputFormat format = OutputFormat.TEXT;
+
   @Option(name = "--show-branch", aliases = {"-b"}, multiValued = true,
       usage = "displays the sha of each project in the specified branch")
   private List<String> showBranch;
@@ -93,6 +101,11 @@
   @Option(name = "--all", usage = "display all projects that are accessible by the calling user")
   private boolean all;
 
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of projects to list")
+  private int limit;
+
+  private String matchPrefix;
+
   @Inject
   protected ListProjects(CurrentUser currentUser, ProjectCache projectCache,
       GitRepositoryManager repoManager,
@@ -115,6 +128,20 @@
     return showDescription;
   }
 
+  public OutputFormat getFormat() {
+    return format;
+  }
+
+  public ListProjects setFormat(OutputFormat fmt) {
+    this.format = fmt;
+    return this;
+  }
+
+  public ListProjects setMatchPrefix(String prefix) {
+    this.matchPrefix = prefix;
+    return this;
+  }
+
   public void display(OutputStream out) {
     final PrintWriter stdout;
     try {
@@ -124,10 +151,14 @@
       throw new RuntimeException("JVM lacks UTF-8 encoding", e);
     }
 
+    int found = 0;
+    Map<String, ProjectInfo> output = Maps.newTreeMap();
+    Map<String, String> hiddenNames = Maps.newHashMap();
+
     final TreeMap<Project.NameKey, ProjectNode> treeMap =
         new TreeMap<Project.NameKey, ProjectNode>();
     try {
-      for (final Project.NameKey projectName : projectCache.all()) {
+      for (final Project.NameKey projectName : scan()) {
         final ProjectState e = projectCache.get(projectName);
         if (e == null) {
           // If we can't get it from the cache, pretend its not present.
@@ -137,18 +168,39 @@
 
         final ProjectControl pctl = e.controlFor(currentUser);
         final boolean isVisible = pctl.isVisible() || (all && pctl.isOwner());
-        if (showTree) {
+        if (showTree && !format.isJson()) {
           treeMap.put(projectName,
               projectNodeFactory.create(pctl.getProject(), isVisible));
           continue;
         }
 
-        if (!isVisible) {
+        if (!isVisible && !(showTree && pctl.isOwner())) {
           // Require the project itself to be visible to the user.
           //
           continue;
         }
 
+        ProjectInfo info = new ProjectInfo();
+        info.name = projectName.get();
+        if (showTree && format.isJson()) {
+          ProjectState parent = e.getParentState();
+          if (parent != null) {
+            ProjectControl parentCtrl = parent.controlFor(currentUser);
+            if (parentCtrl.isVisible() || parentCtrl.isOwner()) {
+              info.parent = parent.getProject().getName();
+            } else {
+              info.parent = hiddenNames.get(parent.getProject().getName());
+              if (info.parent == null) {
+                info.parent = "?-" + (hiddenNames.size() + 1);
+                hiddenNames.put(parent.getProject().getName(), info.parent);
+              }
+            }
+          }
+        }
+        if (showDescription && !e.getProject().getDescription().isEmpty()) {
+          info.description = e.getProject().getDescription();
+        }
+
         try {
           if (showBranch != null) {
             Repository git = repoManager.openRepository(projectName);
@@ -162,20 +214,19 @@
                continue;
               }
 
-              for (Ref ref : refs) {
-                if (ref == null) {
-                  // Print stub (forty '-' symbols)
-                  stdout.print("----------------------------------------");
-                } else {
-                  stdout.print(ref.getObjectId().name());
+              for (int i = 0; i < showBranch.size(); i++) {
+                Ref ref = refs.get(i);
+                if (ref != null && ref.getObjectId() != null) {
+                  if (info.branches == null) {
+                    info.branches = Maps.newLinkedHashMap();
+                  }
+                  info.branches.put(showBranch.get(i), ref.getObjectId().name());
                 }
-                stdout.print(' ');
               }
             } finally {
               git.close();
             }
-
-          } else if (type != FilterType.ALL) {
+          } else if (!showTree && type != FilterType.ALL) {
             Repository git = repoManager.openRepository(projectName);
             try {
               if (!type.matches(git)) {
@@ -194,18 +245,40 @@
           continue;
         }
 
-        stdout.print(projectName.get());
-
-        String desc;
-        if (showDescription && !(desc = e.getProject().getDescription()).isEmpty()) {
-          // We still want to list every project as one-liners, hence escaping \n.
-          stdout.print(" - " + desc.replace("\n", "\\n"));
+        if (limit > 0 && ++found > limit) {
+          break;
         }
 
-        stdout.print("\n");
+        if (format.isJson()) {
+          output.put(info.name, info);
+          continue;
+        }
+
+        if (showBranch != null) {
+          for (String name : showBranch) {
+            String ref = info.branches != null ? info.branches.get(name) : null;
+            if (ref == null) {
+              // Print stub (forty '-' symbols)
+              ref = "----------------------------------------";
+            }
+            stdout.print(ref);
+            stdout.print(' ');
+          }
+        }
+        stdout.print(info.name);
+
+        if (info.description != null) {
+          // We still want to list every project as one-liners, hence escaping \n.
+          stdout.print(" - " + info.description.replace("\n", "\\n"));
+        }
+        stdout.print('\n');
       }
 
-      if (showTree && treeMap.size() > 0) {
+      if (format.isJson()) {
+        format.newGson().toJson(
+            output, new TypeToken<Map<String, ProjectInfo>>() {}.getType(), stdout);
+        stdout.print('\n');
+      } else if (showTree && treeMap.size() > 0) {
         printProjectTree(stdout, treeMap);
       }
     } finally {
@@ -213,6 +286,14 @@
     }
   }
 
+  private Iterable<NameKey> scan() {
+    if (matchPrefix != null) {
+      return projectCache.byName(matchPrefix);
+    } else {
+      return projectCache.all();
+    }
+  }
+
   private void printProjectTree(final PrintWriter stdout,
       final TreeMap<Project.NameKey, ProjectNode> treeMap) {
     final SortedSet<ProjectNode> sortedNodes = new TreeSet<ProjectNode>();
@@ -270,4 +351,11 @@
     }
     return false;
   }
+
+  private static class ProjectInfo {
+    transient String name;
+    String parent;
+    String description;
+    Map<String, String> branches;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
index ce47d2d..77e082d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query;
 
+import com.google.common.collect.Iterables;
 import com.google.gwtorm.server.OrmException;
 
 import java.util.Collection;
@@ -44,23 +45,35 @@
 public abstract class Predicate<T> {
   /** Combine the passed predicates into a single AND node. */
   public static <T> Predicate<T> and(final Predicate<T>... that) {
+    if (that.length == 1) {
+      return that[0];
+    }
     return new AndPredicate<T>(that);
   }
 
   /** Combine the passed predicates into a single AND node. */
   public static <T> Predicate<T> and(
       final Collection<? extends Predicate<T>> that) {
+    if (that.size() == 1) {
+      return Iterables.getOnlyElement(that);
+    }
     return new AndPredicate<T>(that);
   }
 
   /** Combine the passed predicates into a single OR node. */
   public static <T> Predicate<T> or(final Predicate<T>... that) {
+    if (that.length == 1) {
+      return that[0];
+    }
     return new OrPredicate<T>(that);
   }
 
   /** Combine the passed predicates into a single OR node. */
   public static <T> Predicate<T> or(
       final Collection<? extends Predicate<T>> that) {
+    if (that.size() == 1) {
+      return Iterables.getOnlyElement(that);
+    }
     return new OrPredicate<T>(that);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 4d8e806..8c1157e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -44,6 +45,7 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -206,10 +208,7 @@
     }
 
     if ("draft".equalsIgnoreCase(value)) {
-      if (currentUser instanceof IdentifiedUser) {
-        return new HasDraftByPredicate(args.dbProvider,
-            ((IdentifiedUser) currentUser).getAccountId());
-      }
+      return new HasDraftByPredicate(args.dbProvider, self());
     }
 
     throw new IllegalArgumentException();
@@ -233,6 +232,14 @@
       return new IsReviewedPredicate(args.dbProvider);
     }
 
+    if ("owner".equalsIgnoreCase(value)) {
+      return new OwnerPredicate(args.dbProvider, self());
+    }
+
+    if ("reviewer".equalsIgnoreCase(value)) {
+      return new ReviewerPredicate(args.dbProvider, self());
+    }
+
     try {
       return status(value);
     } catch (IllegalArgumentException e) {
@@ -303,42 +310,59 @@
   @Operator
   public Predicate<ChangeData> starredby(String who)
       throws QueryParseException, OrmException {
-    Account account = args.accountResolver.find(who);
-    if (account == null) {
-      throw error("User " + who + " not found");
+    if ("self".equals(who)) {
+      return new IsStarredByPredicate(args.dbProvider, currentUser);
     }
-    return new IsStarredByPredicate(args.dbProvider, //
-        args.userFactory.create(args.dbProvider, account.getId()));
+    Set<Account.Id> m = parseAccount(who);
+    List<IsStarredByPredicate> p = Lists.newArrayListWithCapacity(m.size());
+    for (Account.Id id : m) {
+      p.add(new IsStarredByPredicate(args.dbProvider,
+          args.userFactory.create(args.dbProvider, id)));
+    }
+    return Predicate.or(p);
   }
 
   @Operator
   public Predicate<ChangeData> watchedby(String who)
       throws QueryParseException, OrmException {
-    Account account = args.accountResolver.find(who);
-    if (account == null) {
-      throw error("User " + who + " not found");
+    Set<Account.Id> m = parseAccount(who);
+    List<IsWatchedByPredicate> p = Lists.newArrayListWithCapacity(m.size());
+    for (Account.Id id : m) {
+      if (currentUser instanceof IdentifiedUser
+          && id.equals(((IdentifiedUser) currentUser).getAccountId())) {
+        p.add(new IsWatchedByPredicate(args, currentUser));
+      } else {
+        p.add(new IsWatchedByPredicate(args,
+            args.userFactory.create(args.dbProvider, id)));
+      }
     }
-    return new IsWatchedByPredicate(args, args.userFactory.create(
-        args.dbProvider, account.getId()));
+    return Predicate.or(p);
   }
 
   @Operator
   public Predicate<ChangeData> draftby(String who) throws QueryParseException,
       OrmException {
-    Account account = args.accountResolver.find(who);
-    if (account == null) {
-      throw error("User " + who + " not found");
+    Set<Account.Id> m = parseAccount(who);
+    List<HasDraftByPredicate> p = Lists.newArrayListWithCapacity(m.size());
+    for (Account.Id id : m) {
+      p.add(new HasDraftByPredicate(args.dbProvider, id));
     }
-    return new HasDraftByPredicate(args.dbProvider, account.getId());
+    return Predicate.or(p);
   }
 
   @Operator
   public Predicate<ChangeData> visibleto(String who)
       throws QueryParseException, OrmException {
-    Account account = args.accountResolver.find(who);
-    if (account != null) {
-      return visibleto(args.userFactory
-          .create(args.dbProvider, account.getId()));
+    if ("self".equals(who)) {
+      return is_visible();
+    }
+    Set<Account.Id> m = args.accountResolver.findAll(who);
+    if (!m.isEmpty()) {
+      List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
+      for (Account.Id id : m) {
+        return visibleto(args.userFactory.create(args.dbProvider, id));
+      }
+      return Predicate.or(p);
     }
 
     // If its not an account, maybe its a group?
@@ -375,24 +399,17 @@
   @Operator
   public Predicate<ChangeData> owner(String who) throws QueryParseException,
       OrmException {
-    Set<Account.Id> m = args.accountResolver.findAll(who);
-    if (m.isEmpty()) {
-      throw error("User " + who + " not found");
-    } else if (m.size() == 1) {
-      Account.Id id = m.iterator().next();
-      return new OwnerPredicate(args.dbProvider, id);
-    } else {
-      List<OwnerPredicate> p = new ArrayList<OwnerPredicate>(m.size());
-      for (Account.Id id : m) {
-        p.add(new OwnerPredicate(args.dbProvider, id));
-      }
-      return Predicate.or(p);
+    Set<Account.Id> m = parseAccount(who);
+    List<OwnerPredicate> p = Lists.newArrayListWithCapacity(m.size());
+    for (Account.Id id : m) {
+      p.add(new OwnerPredicate(args.dbProvider, id));
     }
+    return Predicate.or(p);
   }
 
   @Operator
-  public Predicate<ChangeData> ownerin(String group) throws QueryParseException,
-      OrmException {
+  public Predicate<ChangeData> ownerin(String group)
+      throws QueryParseException {
     AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(group));
     if (g == null) {
       throw error("Group " + group + " not found");
@@ -403,24 +420,17 @@
   @Operator
   public Predicate<ChangeData> reviewer(String who)
       throws QueryParseException, OrmException {
-    Set<Account.Id> m = args.accountResolver.findAll(who);
-    if (m.isEmpty()) {
-      throw error("User " + who + " not found");
-    } else if (m.size() == 1) {
-      Account.Id id = m.iterator().next();
-      return new ReviewerPredicate(args.dbProvider, id);
-    } else {
-      List<ReviewerPredicate> p = new ArrayList<ReviewerPredicate>(m.size());
-      for (Account.Id id : m) {
-        p.add(new ReviewerPredicate(args.dbProvider, id));
-      }
-      return Predicate.or(p);
+    Set<Account.Id> m = parseAccount(who);
+    List<ReviewerPredicate> p = Lists.newArrayListWithCapacity(m.size());
+    for (Account.Id id : m) {
+      p.add(new ReviewerPredicate(args.dbProvider, id));
     }
+    return Predicate.or(p);
   }
 
   @Operator
   public Predicate<ChangeData> reviewerin(String group)
-      throws QueryParseException, OrmException {
+      throws QueryParseException {
     AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(group));
     if (g == null) {
       throw error("Group " + group + " not found");
@@ -532,4 +542,23 @@
       throw error("Unsupported query:" + query);
     }
   }
+
+  private Set<Account.Id> parseAccount(String who)
+      throws QueryParseException, OrmException {
+    if ("self".equals(who)) {
+      return Collections.singleton(self());
+    }
+    Set<Account.Id> matches = args.accountResolver.findAll(who);
+    if (matches.isEmpty()) {
+      throw error("User " + who + " not found");
+    }
+    return matches;
+  }
+
+  private Account.Id self() {
+    if (currentUser instanceof IdentifiedUser) {
+      return ((IdentifiedUser) currentUser).getAccountId();
+    }
+    throw new IllegalArgumentException();
+  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index d0bbc72..13e3f17 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -30,11 +30,13 @@
       @Override
       public void run() throws Exception {
         parseCommandLine(impl);
-        if (impl.isShowTree() && (impl.getShowBranch() != null)) {
-          throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
-        }
-        if (impl.isShowTree() && impl.isShowDescription()) {
-          throw new UnloggedFailure(1, "fatal: --tree and --description options are not compatible.");
+        if (!impl.getFormat().isJson()) {
+          if (impl.isShowTree() && (impl.getShowBranch() != null)) {
+            throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
+          }
+          if (impl.isShowTree() && impl.isShowDescription()) {
+            throw new UnloggedFailure(1, "fatal: --tree and --description options are not compatible.");
+          }
         }
         impl.display(out);
       }
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
index de2f7e9..d4174cb 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -46,6 +46,7 @@
 import org.kohsuke.args4j.Option;
 import org.kohsuke.args4j.OptionDef;
 import org.kohsuke.args4j.spi.BooleanOptionHandler;
+import org.kohsuke.args4j.spi.EnumOptionHandler;
 import org.kohsuke.args4j.spi.OptionHandler;
 import org.kohsuke.args4j.spi.Setter;
 
@@ -66,7 +67,6 @@
  * args4j style format prior to invoking args4j for parsing.
  */
 public class CmdLineParser {
-
   public interface Factory {
     CmdLineParser create(Object bean);
   }
@@ -118,6 +118,67 @@
     out.write('\n');
   }
 
+  public void printQueryStringUsage(String name, StringWriter out) {
+    out.write(name);
+
+    char next = '?';
+    List<NamedOptionDef> booleans = new ArrayList<NamedOptionDef>();
+    for (@SuppressWarnings("rawtypes") OptionHandler handler : parser.options) {
+      if (handler.option instanceof NamedOptionDef) {
+        NamedOptionDef n = (NamedOptionDef) handler.option;
+
+        if (handler instanceof BooleanOptionHandler) {
+          booleans.add(n);
+          continue;
+        }
+
+        if (!n.required()) {
+          out.write('[');
+        }
+        out.write(next);
+        next = '&';
+        if (n.name().startsWith("--")) {
+          out.write(n.name().substring(2));
+        } else if (n.name().startsWith("-")) {
+          out.write(n.name().substring(1));
+        } else {
+          out.write(n.name());
+        }
+        out.write('=');
+
+        String var = handler.getDefaultMetaVariable();
+        if (handler instanceof EnumOptionHandler) {
+          var = var.substring(1, var.length() - 1);
+          var = var.replaceAll(" ", "");
+        }
+        out.write(var);
+        if (!n.required()) {
+          out.write(']');
+        }
+        if (n.isMultiValued()) {
+          out.write('*');
+        }
+      }
+    }
+    for (NamedOptionDef n : booleans) {
+      if (!n.required()) {
+        out.write('[');
+      }
+      out.write(next);
+      next = '&';
+      if (n.name().startsWith("--")) {
+        out.write(n.name().substring(2));
+      } else if (n.name().startsWith("-")) {
+        out.write(n.name().substring(1));
+      } else {
+        out.write(n.name());
+      }
+      if (!n.required()) {
+        out.write(']');
+      }
+    }
+  }
+
   public boolean wasHelpRequestedByOption() {
     return parser.help.value;
   }
diff --git a/pom.xml b/pom.xml
index 1933329..377b212 100644
--- a/pom.xml
+++ b/pom.xml
@@ -50,7 +50,7 @@
     <gwtormVersion>1.4</gwtormVersion>
     <gwtjsonrpcVersion>1.3</gwtjsonrpcVersion>
     <gwtexpuiVersion>1.2.5</gwtexpuiVersion>
-    <gwtVersion>2.3.0</gwtVersion>
+    <gwtVersion>2.4.0</gwtVersion>
     <slf4jVersion>1.6.1</slf4jVersion>
     <guiceVersion>3.0</guiceVersion>
     <jettyVersion>7.2.1.v20101111</jettyVersion>
@@ -363,7 +363,7 @@
         <plugin>
           <groupId>org.codehaus.mojo</groupId>
           <artifactId>gwt-maven-plugin</artifactId>
-          <version>2.3.0</version>
+          <version>2.4.0</version>
         </plugin>
 
         <plugin>
@@ -444,6 +444,12 @@
   <dependencyManagement>
     <dependencies>
       <dependency>
+        <groupId>com.google.code.gson</groupId>
+        <artifactId>gson</artifactId>
+        <version>2.1</version>
+      </dependency>
+
+      <dependency>
         <groupId>gwtorm</groupId>
         <artifactId>gwtorm</artifactId>
         <version>${gwtormVersion}</version>
@@ -825,11 +831,6 @@
     </repository>
 
     <repository>
-      <id>gson</id>
-      <url>https://google-gson.googlecode.com/svn/mavenrepo/</url>
-    </repository>
-
-    <repository>
       <id>objectweb-repository</id>
       <url>http://maven.objectweb.org/maven2/</url>
     </repository>