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
This commit is contained in:
Shawn O. Pearce
2012-04-23 09:14:00 -07:00
committed by gerrit code review
38 changed files with 1711 additions and 223 deletions

View File

@@ -23,7 +23,7 @@ group, all projects are listed.
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 @@ Line-feeds are escaped to allow ls-project to keep the
`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 @@ Line-feeds are escaped to allow ls-project to keep the
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
=====

View File

@@ -18,6 +18,7 @@ User Guide
* 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

134
Documentation/rest-api.txt Normal file
View File

@@ -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]

View File

@@ -75,7 +75,8 @@ Change-Id that was scraped out of the commit message.
[[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 @@ Changes originally submitted by a user in 'GROUP'.
[[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 @@ is:reviewed::
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 @@ the change. This flag is always added to any query.
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'::
+

View File

@@ -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 @@ public class GlobalCapability {
/** 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. */

View File

@@ -26,9 +26,6 @@ import java.util.List;
@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);

View File

@@ -56,6 +56,10 @@ public class RpcStatus implements RpcStartHandler, RpcCompleteHandler {
@Override
public void onRpcStart(final RpcStartEvent event) {
onRpcStart();
}
public void onRpcStart() {
if (++activeCalls == 1) {
if (hideDepth == 0) {
loading.setVisible(true);
@@ -65,6 +69,10 @@ public class RpcStatus implements RpcStartHandler, RpcCompleteHandler {
@Override
public void onRpcComplete(final RpcCompleteEvent event) {
onRpcComplete();
}
public void onRpcComplete() {
if (--activeCalls == 0) {
loading.setVisible(false);
}

View File

@@ -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; }-*/;
}

View File

@@ -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.ProjectNameSuggestOracle;
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 @@ public class MyWatchedProjectsScreen extends SettingsScreen implements
// 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 @@ public class MyWatchedProjectsScreen extends SettingsScreen implements
}
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();

View File

@@ -17,6 +17,8 @@ package com.google.gerrit.client.admin;
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.gwt.user.client.ui.VerticalPanel;
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 @@ public class CreateProjectScreen extends Screen {
}
@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();
}
}

View File

@@ -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 @@ public class GroupListScreen extends AccountScreen {
@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();
}

View File

@@ -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 @@ public class ProjectListScreen extends Screen {
@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 @@ public class ProjectListScreen extends Screen {
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);
}

View File

@@ -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() {
}
}

View File

@@ -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() {
}
}

View File

@@ -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; }-*/;
}

View File

@@ -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;
}
}
}-*/;
}

View File

@@ -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() {
}
}

View File

@@ -0,0 +1,174 @@
// Copyright (C) 2012 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.client.rpc;
import com.google.gerrit.client.RpcStatus;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.http.client.URL;
import com.google.gwt.user.client.rpc.StatusCodeException;
import com.google.gwtjsonrpc.client.RemoteJsonException;
import com.google.gwtjsonrpc.client.ServerUnavailableException;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.JsonConstants;
/** Makes a REST API call to the server. */
public class RestApi {
/**
* Expected JSON content body prefix that prevents XSSI.
* <p>
* The server always includes this line as the first line of the response
* content body when the response body is formatted as JSON. It gets inserted
* by the server to prevent the resource from being imported into another
* domain's page using a &lt;script&gt; tag. This line must be removed before
* the JSON can be parsed.
*/
private static final String JSON_MAGIC = ")]}'\n";
private StringBuilder url;
private boolean hasQueryParams;
/**
* Initialize a new API call.
* <p>
* By default the JSON format will be selected by including an HTTP Accept
* header in the request.
*
* @param name URL of the REST resource to access, e.g. {@code "/projects/"}
* to list accessible projects from the server.
*/
public RestApi(String name) {
if (name.startsWith("/")) {
name = name.substring(1);
}
url = new StringBuilder();
url.append(GWT.getHostPageBaseURL());
url.append(name);
}
public RestApi addParameter(String name, String value) {
return addParameterRaw(name, URL.encodeQueryString(value));
}
public RestApi addParameterTrue(String name) {
return addParameterRaw(name, null);
}
public RestApi addParameter(String name, boolean value) {
return addParameterRaw(name, value ? "t" : "f");
}
public RestApi addParameter(String name, int value) {
return addParameterRaw(name, String.valueOf(value));
}
public RestApi addParameterRaw(String name, String value) {
if (hasQueryParams) {
url.append("&");
} else {
url.append("?");
hasQueryParams = true;
}
url.append(name);
if (value != null) {
url.append("=").append(value);
}
return this;
}
public <T extends JavaScriptObject> void send(final AsyncCallback<T> cb) {
RequestBuilder req = new RequestBuilder(RequestBuilder.GET, url.toString());
req.setHeader("Accept", JsonConstants.JSON_TYPE);
req.setCallback(new RequestCallback() {
@Override
public void onResponseReceived(Request req, Response res) {
RpcStatus.INSTANCE.onRpcComplete();
int status = res.getStatusCode();
if (status != 200) {
if ((400 <= status && status < 500) && isTextBody(res)) {
cb.onFailure(new RemoteJsonException(res.getText(), status, null));
} else {
cb.onFailure(new StatusCodeException(status, res.getStatusText()));
}
return;
}
if (!isJsonBody(res)) {
cb.onFailure(new RemoteJsonException("Invalid JSON"));
return;
}
String json = res.getText();
if (!json.startsWith(JSON_MAGIC)) {
cb.onFailure(new RemoteJsonException("Invalid JSON"));
return;
}
T data;
try {
data = Natives.parseJSON(json.substring(JSON_MAGIC.length()));
} catch (RuntimeException e) {
cb.onFailure(new RemoteJsonException("Invalid JSON"));
return;
}
cb.onSuccess(data);
}
@Override
public void onError(Request req, Throwable err) {
RpcStatus.INSTANCE.onRpcComplete();
if (err.getMessage().contains("XmlHttpRequest.status")) {
cb.onFailure(new ServerUnavailableException());
} else {
cb.onFailure(err);
}
}
});
try {
RpcStatus.INSTANCE.onRpcStart();
req.send();
} catch (RequestException e) {
RpcStatus.INSTANCE.onRpcComplete();
cb.onFailure(e);
}
}
private static boolean isJsonBody(Response res) {
return isContentType(res, JsonConstants.JSON_TYPE);
}
private static boolean isTextBody(Response res) {
return isContentType(res, "text/plain");
}
private static boolean isContentType(Response res, String want) {
String type = res.getHeader("Content-Type");
if (type == null) {
return false;
}
int semi = type.indexOf(';');
if (semi >= 0) {
type = type.substring(0, semi).trim();
}
return want.equals(type);
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}
}

View File

@@ -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 @@ public class ProjectsTable extends NavigationTable<Project> {
fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
}
@Override
protected MyFlexTable createFlexTable() {
MyFlexTable table = new MyFlexTable() {
@Override
@@ -78,8 +82,8 @@ public class ProjectsTable extends NavigationTable<Project> {
}
@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 class ProjectsTable extends NavigationTable<Project> {
}
}
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 @@ public class ProjectsTable extends NavigationTable<Project> {
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);
}

View File

@@ -20,12 +20,10 @@ import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
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.ServletRequest;
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 @@ class ContainerAuthFilter implements Filter {
@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 @@ class ContainerAuthFilter implements Filter {
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) {

View File

@@ -41,5 +41,7 @@ public class GitOverHttpModule extends ServletModule {
String git = GitOverHttpServlet.URL_REGEX;
filterRegex(git).through(authFilter);
serveRegex(git).with(GitOverHttpServlet.class);
filter("/a/*").through(authFilter);
}
}

View File

@@ -30,7 +30,6 @@ 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;
@@ -100,11 +99,6 @@ class ProjectDigestFilter implements Filter {
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)) {

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,178 @@
// Copyright (C) 2012 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.httpd;
import com.google.common.base.Strings;
import com.google.gerrit.util.cli.CmdLineParser;
import com.google.gwtjsonrpc.server.RPCServletUtils;
import com.google.gwtjsonrpc.common.JsonConstants;
import com.google.inject.Inject;
import org.kohsuke.args4j.CmdLineException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public abstract class RestApiServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger log =
LoggerFactory.getLogger(RestApiServlet.class);
/** MIME type used for a JSON response body. */
protected static final String JSON_TYPE = JsonConstants.JSON_TYPE;
/**
* Garbage prefix inserted before JSON output to prevent XSSI.
* <p>
* This prefix is ")]}'\n" and is designed to prevent a web browser from
* executing the response body if the resource URI were to be referenced using
* a &lt;script src="...&gt; HTML tag from another web site. Clients using the
* HTTP interface will need to always strip the first line of response data to
* remove this magic header.
*/
protected static final byte[] JSON_MAGIC;
static {
try {
JSON_MAGIC = ")]}'\n".getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("UTF-8 not supported", e);
}
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
noCache(res);
try {
super.service(req, res);
} catch (Error err) {
handleError(err, req, res);
} catch (RuntimeException err) {
handleError(err, req, res);
}
}
private static void noCache(HttpServletResponse res) {
res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
res.setHeader("Pragma", "no-cache");
res.setHeader("Cache-Control", "no-cache, must-revalidate");
res.setHeader("Content-Disposition", "attachment");
}
private static void handleError(
Throwable err, HttpServletRequest req, HttpServletResponse res)
throws IOException {
String uri = req.getRequestURI();
if (!Strings.isNullOrEmpty(req.getQueryString())) {
uri += "?" + req.getQueryString();
}
log.error(String.format("Error in %s %s", req.getMethod(), uri), err);
if (!res.isCommitted()) {
res.reset();
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
noCache(res);
sendText(req, res, "Internal Server Error");
}
}
protected static boolean acceptsJson(HttpServletRequest req) {
String accept = req.getHeader("Accept");
if (accept == null) {
return false;
} else if (JSON_TYPE.equals(accept)) {
return true;
} else if (accept.startsWith(JSON_TYPE + ",")) {
return true;
}
for (String p : accept.split("[ ,;][ ,;]*")) {
if (JSON_TYPE.equals(p)) {
return true;
}
}
return false;
}
protected static void sendText(HttpServletRequest req,
HttpServletResponse res, String data) throws IOException {
res.setContentType("text/plain");
res.setCharacterEncoding("UTF-8");
send(req, res, data.getBytes("UTF-8"));
}
protected static void send(HttpServletRequest req, HttpServletResponse res,
byte[] data) throws IOException {
if (data.length > 256 && RPCServletUtils.acceptsGzipEncoding(req)) {
res.setHeader("Content-Encoding", "gzip");
data = HtmlDomUtil.compress(data);
}
res.setContentLength(data.length);
OutputStream out = res.getOutputStream();
try {
out.write(data);
} finally {
out.close();
}
}
public static class ParameterParser {
private final CmdLineParser.Factory parserFactory;
@Inject
ParameterParser(CmdLineParser.Factory pf) {
this.parserFactory = pf;
}
public <T> boolean parse(T param, HttpServletRequest req,
HttpServletResponse res) throws IOException {
CmdLineParser clp = parserFactory.create(param);
try {
@SuppressWarnings("unchecked")
Map<String, String[]> parameterMap = req.getParameterMap();
clp.parseOptionMap(parameterMap);
} catch (CmdLineException e) {
if (!clp.wasHelpRequestedByOption()) {
res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
sendText(req, res, e.getMessage());
return false;
}
}
if (clp.wasHelpRequestedByOption()) {
StringWriter msg = new StringWriter();
clp.printQueryStringUsage(req.getRequestURI(), msg);
msg.write('\n');
msg.write('\n');
clp.printUsage(msg, null);
msg.write('\n');
sendText(req, res, msg.toString());
return false;
}
return true;
}
}
}

View File

@@ -23,6 +23,8 @@ import com.google.gerrit.httpd.raw.LegacyGerritServlet;
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 @@ class UrlModule extends ServletModule {
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() {

View File

@@ -38,6 +38,7 @@ import com.google.gerrit.server.contact.ContactStore;
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 @@ public class WebModule extends FactoryModule {
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(

View File

@@ -39,8 +39,6 @@ import com.google.gerrit.server.patch.AddReviewer;
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 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
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 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
@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 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
@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 @@ class SuggestServiceImpl extends BaseServiceImplementation implements
}
}
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;
}

View File

@@ -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();
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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();
}
}

View File

@@ -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.PrintWriter;
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 @@ public class ListProjects {
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 @@ public class ListProjects {
@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 @@ public class ListProjects {
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 @@ public class ListProjects {
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 @@ public class ListProjects {
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 @@ public class ListProjects {
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 @@ public class ListProjects {
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 @@ public class ListProjects {
}
}
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 @@ public class ListProjects {
}
return false;
}
private static class ProjectInfo {
transient String name;
String parent;
String description;
Map<String, String> branches;
}
}

View File

@@ -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 @@ import java.util.List;
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);
}

View File

@@ -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 org.eclipse.jgit.lib.AbbreviatedObjectId;
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 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
}
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 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
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 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
@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 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
@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 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
@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 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
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();
}
}

View File

@@ -30,11 +30,13 @@ final class ListProjectsCommand extends BaseCommand {
@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);
}

View File

@@ -46,6 +46,7 @@ import org.kohsuke.args4j.NamedOptionDef;
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 @@ import java.util.ResourceBundle;
* args4j style format prior to invoking args4j for parsing.
*/
public class CmdLineParser {
public interface Factory {
CmdLineParser create(Object bean);
}
@@ -118,6 +118,67 @@ public class CmdLineParser {
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;
}

15
pom.xml
View File

@@ -50,7 +50,7 @@ limitations under the License.
<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 @@ limitations under the License.
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>gwt-maven-plugin</artifactId>
<version>2.3.0</version>
<version>2.4.0</version>
</plugin>
<plugin>
@@ -443,6 +443,12 @@ limitations under the License.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.1</version>
</dependency>
<dependency>
<groupId>gwtorm</groupId>
<artifactId>gwtorm</artifactId>
@@ -824,11 +830,6 @@ limitations under the License.
<url>http://download.java.net/maven/2/</url>
</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>