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 <script> 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 <script src="...> 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>