Add interface for ListProjects REST endpoint
ListProjects is a legacy REST endpoint that was used to list/query
projects before we had a project index, but it's still widely used (e.g.
from the ls-projects SSH command or from Gerrit's web frontend to
populate the repository list). With QueryProjects we have a REST
endpoint that uses the project index for querying projects which should
be used instead, but it doesn't support all functionality of the
ListProject REST endpoint.
At Google we want to deprecate most of the options that the ListProjects
REST endpoint supports so that ListProjects can delegate all requests to
QueryProjects. Doing this is a backwards incompatible change which will
break the REST API and the SSH API for listing projects. This is an API
that is known to have lots of usage by automation tools and hence
breaking this API for everyone is not an option.
By adding an interface for ListProjects we can replace the REST endpoint
implementation internally at Google, so that this breakage only affects
Google, but no one else.
Unfortunately it's not possible to apply @Option annotations on the
interface methods. To avoid that all implementations need to define the
options by themselves we add an AbstractListProjects implementation that
defines the options and that can be extended by ListProjects
implementations.
The ListProjects interface is used everywhere where projects need to be
listed, except from the ls-projects SSH command which has special needs.
This is fine for us (Google) since we are not using SSH.
Dynamic options beans need to be bound on the ListProjects
implementation class.
Release-Notes: skip
Bug: Google b/289490702
Change-Id: Ibf963cf3db7e02af1996e13bf59a67c38a182e2a
Signed-off-by: Edwin Kempin <ekempin@google.com>
diff --git a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
index f311b35..78cf811 100644
--- a/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
+++ b/java/com/google/gerrit/server/api/projects/ProjectsImpl.java
@@ -24,7 +24,6 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
-import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.restapi.project.ListProjects;
import com.google.gerrit.server.restapi.project.ListProjects.FilterType;
import com.google.gerrit.server.restapi.project.ProjectsCollection;
@@ -94,8 +93,7 @@
};
}
- private SortedMap<String, ProjectInfo> list(ListRequest request)
- throws RestApiException, PermissionBackendException {
+ private SortedMap<String, ProjectInfo> list(ListRequest request) throws Exception {
ListProjects lp = listProvider.get();
lp.setShowDescription(request.getDescription());
lp.setLimit(request.getLimit());
diff --git a/java/com/google/gerrit/server/restapi/project/AbstractListProjects.java b/java/com/google/gerrit/server/restapi/project/AbstractListProjects.java
new file mode 100644
index 0000000..6467d81
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/AbstractListProjects.java
@@ -0,0 +1,118 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.project;
+
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.json.OutputFormat;
+import org.kohsuke.args4j.Option;
+
+/**
+ * Base class for {@link ListProjects} implementations.
+ *
+ * <p>Defines the options that are supported by the list projects REST endpoint.
+ */
+public abstract class AbstractListProjects implements ListProjects {
+ @Override
+ @Option(name = "--format", usage = "(deprecated) output format")
+ public abstract void setFormat(OutputFormat fmt);
+
+ @Override
+ @Option(
+ name = "--show-branch",
+ aliases = {"-b"},
+ usage = "displays the sha of each project in the specified branch")
+ public abstract void addShowBranch(String branch);
+
+ @Override
+ @Option(
+ name = "--tree",
+ aliases = {"-t"},
+ usage =
+ "displays project inheritance in a tree-like format\n"
+ + "this option does not work together with the show-branch option")
+ public abstract void setShowTree(boolean showTree);
+
+ @Override
+ @Option(name = "--type", usage = "type of project")
+ public abstract void setFilterType(FilterType type);
+
+ @Override
+ @Option(
+ name = "--description",
+ aliases = {"-d"},
+ usage = "include description of project in list")
+ public abstract void setShowDescription(boolean showDescription);
+
+ @Override
+ @Option(name = "--all", usage = "display all projects that are accessible by the calling user")
+ public abstract void setAll(boolean all);
+
+ @Override
+ @Option(
+ name = "--state",
+ aliases = {"-s"},
+ usage = "filter by project state")
+ public abstract void setState(com.google.gerrit.extensions.client.ProjectState state);
+
+ @Override
+ @Option(
+ name = "--limit",
+ aliases = {"-n"},
+ metaVar = "CNT",
+ usage = "maximum number of projects to list")
+ public abstract void setLimit(int limit);
+
+ @Override
+ @Option(
+ name = "--start",
+ aliases = {"-S"},
+ metaVar = "CNT",
+ usage = "number of projects to skip")
+ public abstract void setStart(int start);
+
+ @Override
+ @Option(
+ name = "--prefix",
+ aliases = {"-p"},
+ metaVar = "PREFIX",
+ usage = "match project prefix")
+ public abstract void setMatchPrefix(String matchPrefix);
+
+ @Override
+ @Option(
+ name = "--match",
+ aliases = {"-m"},
+ metaVar = "MATCH",
+ usage = "match project substring")
+ public abstract void setMatchSubstring(String matchSubstring);
+
+ @Override
+ @Option(name = "-r", metaVar = "REGEX", usage = "match project regex")
+ public abstract void setMatchRegex(String matchRegex);
+
+ @Override
+ @Option(
+ name = "--has-acl-for",
+ metaVar = "GROUP",
+ usage = "displays only projects on which access rights for this group are directly assigned")
+ public abstract void setGroupUuid(AccountGroup.UUID groupUuid);
+
+ @Override
+ public Response<Object> apply(TopLevelResource resource) throws Exception {
+ return Response.ok(apply());
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjects.java b/java/com/google/gerrit/server/restapi/project/ListProjects.java
index 61c6a67..3ce8708 100644
--- a/java/com/google/gerrit/server/restapi/project/ListProjects.java
+++ b/java/com/google/gerrit/server/restapi/project/ListProjects.java
@@ -14,89 +14,25 @@
package com.google.gerrit.server.restapi.project;
-import static com.google.common.base.Strings.emptyToNull;
-import static com.google.common.base.Strings.isNullOrEmpty;
-import static com.google.common.collect.Ordering.natural;
-import static com.google.gerrit.extensions.client.ProjectState.HIDDEN;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSortedMap;
-import com.google.common.collect.Iterables;
-import com.google.common.flogger.FluentLogger;
-import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccountGroup;
-import com.google.gerrit.entities.GroupReference;
-import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
-import com.google.gerrit.exceptions.NoSuchGroupException;
-import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.extensions.common.ProjectInfo;
-import com.google.gerrit.extensions.common.WebLinkInfo;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.BinaryResult;
-import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.extensions.restapi.TopLevelResource;
-import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.json.OutputFormat;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.WebLinks;
-import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.group.GroupResolver;
-import com.google.gerrit.server.ioutil.RegexListSearcher;
-import com.google.gerrit.server.ioutil.StringUtil;
-import com.google.gerrit.server.permissions.PermissionBackend;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.permissions.ProjectPermission;
-import com.google.gerrit.server.permissions.RefPermission;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.util.TreeFormatter;
-import com.google.gson.reflect.TypeToken;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import java.io.BufferedWriter;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.NavigableSet;
-import java.util.Optional;
import java.util.SortedMap;
-import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.stream.Stream;
-import java.util.stream.StreamSupport;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
-import org.kohsuke.args4j.Option;
/**
* List projects visible to the calling user.
*
* <p>Implement {@code GET /projects/}, without a {@code query=} parameter.
*/
-public class ListProjects implements RestReadView<TopLevelResource> {
- private static final FluentLogger logger = FluentLogger.forEnclosingClass();
-
+public interface ListProjects extends RestReadView<TopLevelResource> {
public enum FilterType {
CODE {
@Override
@@ -140,590 +76,36 @@
abstract boolean useMatch();
}
- private final CurrentUser currentUser;
- private final ProjectCache projectCache;
- private final GroupResolver groupResolver;
- private final GroupControl.Factory groupControlFactory;
- private final GitRepositoryManager repoManager;
- private final PermissionBackend permissionBackend;
- private final ProjectNode.Factory projectNodeFactory;
- private final WebLinks webLinks;
+ void setFormat(OutputFormat fmt);
- @Option(name = "--format", usage = "(deprecated) output format")
- public void setFormat(OutputFormat fmt) {
- format = fmt;
- }
+ void addShowBranch(String branch);
- @Option(
- name = "--show-branch",
- aliases = {"-b"},
- usage = "displays the sha of each project in the specified branch")
- public void addShowBranch(String branch) {
- showBranch.add(branch);
- }
+ void setShowTree(boolean showTree);
- @Option(
- name = "--tree",
- aliases = {"-t"},
- usage =
- "displays project inheritance in a tree-like format\n"
- + "this option does not work together with the show-branch option")
- public void setShowTree(boolean showTree) {
- this.showTree = showTree;
- }
+ void setFilterType(FilterType type);
- @Option(name = "--type", usage = "type of project")
- public void setFilterType(FilterType type) {
- this.type = type;
- }
+ void setShowDescription(boolean showDescription);
- @Option(
- name = "--description",
- aliases = {"-d"},
- usage = "include description of project in list")
- public void setShowDescription(boolean showDescription) {
- this.showDescription = showDescription;
- }
+ void setAll(boolean all);
- @Option(name = "--all", usage = "display all projects that are accessible by the calling user")
- public void setAll(boolean all) {
- this.all = all;
- }
+ void setState(com.google.gerrit.extensions.client.ProjectState state);
- @Option(
- name = "--state",
- aliases = {"-s"},
- usage = "filter by project state")
- public void setState(com.google.gerrit.extensions.client.ProjectState state) {
- this.state = state;
- }
+ void setLimit(int limit);
- @Option(
- name = "--limit",
- aliases = {"-n"},
- metaVar = "CNT",
- usage = "maximum number of projects to list")
- public void setLimit(int limit) {
- this.limit = limit;
- }
+ void setStart(int start);
- @Option(
- name = "--start",
- aliases = {"-S"},
- metaVar = "CNT",
- usage = "number of projects to skip")
- public void setStart(int start) {
- this.start = start;
- }
+ void setMatchPrefix(String matchPrefix);
- @Option(
- name = "--prefix",
- aliases = {"-p"},
- metaVar = "PREFIX",
- usage = "match project prefix")
- public void setMatchPrefix(String matchPrefix) {
- this.matchPrefix = matchPrefix;
- }
+ void setMatchSubstring(String matchSubstring);
- @Option(
- name = "--match",
- aliases = {"-m"},
- metaVar = "MATCH",
- usage = "match project substring")
- public void setMatchSubstring(String matchSubstring) {
- this.matchSubstring = matchSubstring;
- }
+ void setMatchRegex(String matchRegex);
- @Option(name = "-r", metaVar = "REGEX", usage = "match project regex")
- public void setMatchRegex(String matchRegex) {
- this.matchRegex = matchRegex;
- }
-
- @Option(
- name = "--has-acl-for",
- metaVar = "GROUP",
- usage = "displays only projects on which access rights for this group are directly assigned")
- public void setGroupUuid(AccountGroup.UUID groupUuid) {
- this.groupUuid = groupUuid;
- }
-
- @Deprecated private OutputFormat format = OutputFormat.TEXT;
- private final List<String> showBranch = new ArrayList<>();
- private boolean showTree;
- private FilterType type = FilterType.ALL;
- private boolean showDescription;
- private boolean all;
- private com.google.gerrit.extensions.client.ProjectState state;
- private int limit;
- private int start;
- private String matchPrefix;
- private String matchSubstring;
- private String matchRegex;
- private AccountGroup.UUID groupUuid;
- private final Provider<QueryProjects> queryProjectsProvider;
- private final boolean listProjectsFromIndex;
-
- @Inject
- protected ListProjects(
- CurrentUser currentUser,
- ProjectCache projectCache,
- GroupResolver groupResolver,
- GroupControl.Factory groupControlFactory,
- GitRepositoryManager repoManager,
- PermissionBackend permissionBackend,
- ProjectNode.Factory projectNodeFactory,
- WebLinks webLinks,
- Provider<QueryProjects> queryProjectsProvider,
- @GerritServerConfig Config config) {
- this.currentUser = currentUser;
- this.projectCache = projectCache;
- this.groupResolver = groupResolver;
- this.groupControlFactory = groupControlFactory;
- this.repoManager = repoManager;
- this.permissionBackend = permissionBackend;
- this.projectNodeFactory = projectNodeFactory;
- this.webLinks = webLinks;
- this.queryProjectsProvider = queryProjectsProvider;
- this.listProjectsFromIndex = config.getBoolean("gerrit", "listProjectsFromIndex", false);
- }
-
- public List<String> getShowBranch() {
- return showBranch;
- }
-
- public boolean isShowTree() {
- return showTree;
- }
-
- public boolean isShowDescription() {
- return showDescription;
- }
-
- public OutputFormat getFormat() {
- return format;
- }
+ void setGroupUuid(AccountGroup.UUID groupUuid);
@Override
- public Response<Object> apply(TopLevelResource resource)
- throws BadRequestException, PermissionBackendException {
- if (format == OutputFormat.TEXT) {
- ByteArrayOutputStream buf = new ByteArrayOutputStream();
- displayToStream(buf);
- return Response.ok(
- BinaryResult.create(buf.toByteArray())
- .setContentType("text/plain")
- .setCharacterEncoding(UTF_8));
- }
+ default Response<Object> apply(TopLevelResource resource) throws Exception {
return Response.ok(apply());
}
- public SortedMap<String, ProjectInfo> apply()
- throws BadRequestException, PermissionBackendException {
- Optional<String> projectQuery = expressAsProjectsQuery();
- if (projectQuery.isPresent()) {
- return applyAsQuery(projectQuery.get());
- }
-
- format = OutputFormat.JSON;
- return display(null);
- }
-
- private Optional<String> expressAsProjectsQuery() {
- return listProjectsFromIndex
- && !all
- && state != HIDDEN
- && isNullOrEmpty(matchPrefix)
- && isNullOrEmpty(matchRegex)
- && isNullOrEmpty(
- matchSubstring) // TODO: see https://issues.gerritcodereview.com/issues/40010295
- && type == FilterType.ALL
- && showBranch.isEmpty()
- && !showTree
- ? Optional.of(stateToQuery())
- : Optional.empty();
- }
-
- private String stateToQuery() {
- return state != null
- ? String.format("(state:%s)", state.name())
- : "(state:active OR state:read-only)";
- }
-
- private SortedMap<String, ProjectInfo> applyAsQuery(String query) throws BadRequestException {
- try {
- return queryProjectsProvider.get().withQuery(query).withStart(start).withLimit(limit).apply()
- .stream()
- .collect(
- ImmutableSortedMap.toImmutableSortedMap(
- natural(), p -> p.name, p -> showDescription ? p : nullifyDescription(p)));
- } catch (StorageException | MethodNotAllowedException e) {
- logger.atWarning().withCause(e).log(
- "Internal error while processing the query '%s' request", query);
- throw new BadRequestException("Internal error while processing the query request");
- }
- }
-
- private ProjectInfo nullifyDescription(ProjectInfo p) {
- p.description = null;
- return p;
- }
-
- private void printQueryResults(String query, PrintWriter out) throws BadRequestException {
- try {
- if (format.isJson()) {
- format.newGson().toJson(applyAsQuery(query), out);
- } else {
- newProjectsNamesStream(query).forEach(out::println);
- }
- out.flush();
- } catch (StorageException | MethodNotAllowedException e) {
- logger.atWarning().withCause(e).log(
- "Internal error while processing the query '%s' request", query);
- throw new BadRequestException("Internal error while processing the query request");
- }
- }
-
- private Stream<String> newProjectsNamesStream(String query)
- throws MethodNotAllowedException, BadRequestException {
- Stream<String> projects =
- queryProjectsProvider.get().withQuery(query).apply().stream().map(p -> p.name).skip(start);
- if (limit > 0) {
- projects = projects.limit(limit);
- }
-
- return projects;
- }
-
- public void displayToStream(OutputStream displayOutputStream)
- throws BadRequestException, PermissionBackendException {
- PrintWriter stdout =
- new PrintWriter(new BufferedWriter(new OutputStreamWriter(displayOutputStream, UTF_8)));
- Optional<String> projectsQuery = expressAsProjectsQuery();
-
- if (projectsQuery.isPresent()) {
- printQueryResults(projectsQuery.get(), stdout);
- } else {
- display(stdout);
- }
- }
-
- @Nullable
- public SortedMap<String, ProjectInfo> display(@Nullable PrintWriter stdout)
- throws BadRequestException, PermissionBackendException {
- if (all && state != null) {
- throw new BadRequestException("'all' and 'state' may not be used together");
- }
- if (!isGroupVisible()) {
- return Collections.emptySortedMap();
- }
-
- int foundIndex = 0;
- int found = 0;
- TreeMap<String, ProjectInfo> output = new TreeMap<>();
- Map<String, String> hiddenNames = new HashMap<>();
- Map<Project.NameKey, Boolean> accessibleParents = new HashMap<>();
- PermissionBackend.WithUser perm = permissionBackend.user(currentUser);
- final TreeMap<Project.NameKey, ProjectNode> treeMap = new TreeMap<>();
- try {
- Iterator<ProjectState> projectStatesIt = filter(perm).iterator();
- while (projectStatesIt.hasNext()) {
- ProjectState e = projectStatesIt.next();
- Project.NameKey projectName = e.getNameKey();
- if (e.getProject().getState() == HIDDEN && !all && state != HIDDEN) {
- // If we can't get it from the cache, pretend it's not present.
- // If all wasn't selected, and it's HIDDEN, pretend it's not present.
- // If state HIDDEN wasn't selected, and it's HIDDEN, pretend it's not present.
- continue;
- }
-
- if (state != null && e.getProject().getState() != state) {
- continue;
- }
-
- if (groupUuid != null
- && !e.getLocalGroups()
- .contains(GroupReference.forGroup(groupResolver.parseId(groupUuid.get())))) {
- continue;
- }
-
- if (showTree && !format.isJson()) {
- treeMap.put(projectName, projectNodeFactory.create(e.getProject(), true));
- continue;
- }
-
- if (foundIndex++ < start) {
- continue;
- }
- if (limit > 0 && ++found > limit) {
- break;
- }
-
- ProjectInfo info = new ProjectInfo();
- info.name = projectName.get();
- if (showTree && format.isJson()) {
- addParentProjectInfo(hiddenNames, accessibleParents, perm, e, info);
- }
-
- if (showDescription) {
- info.description = emptyToNull(e.getProject().getDescription());
- }
- info.state = e.getProject().getState();
-
- try {
- if (!showBranch.isEmpty()) {
- try (Repository git = repoManager.openRepository(projectName)) {
- if (!type.matches(git)) {
- continue;
- }
-
- List<Ref> refs = retrieveBranchRefs(e, git);
- if (!hasValidRef(refs)) {
- continue;
- }
-
- addProjectBranchesInfo(info, refs);
- }
- } else if (!showTree && type.useMatch()) {
- try (Repository git = repoManager.openRepository(projectName)) {
- if (!type.matches(git)) {
- continue;
- }
- }
- }
- } catch (RepositoryNotFoundException err) {
- // If the Git repository is gone, the project doesn't actually exist anymore.
- continue;
- } catch (IOException err) {
- logger.atWarning().withCause(err).log("Unexpected error reading %s", projectName);
- continue;
- }
-
- ImmutableList<WebLinkInfo> links = webLinks.getProjectLinks(projectName.get());
- info.webLinks = links.isEmpty() ? null : links;
-
- if (stdout == null || format.isJson()) {
- output.put(info.name, info);
- continue;
- }
-
- if (!showBranch.isEmpty()) {
- printProjectBranches(stdout, info);
- }
- stdout.print(info.name);
-
- if (info.description != null) {
- // We still want to list every project as one-liners, hence escaping \n.
- stdout.print(" - " + StringUtil.escapeString(info.description));
- }
- stdout.print('\n');
- }
-
- for (ProjectInfo info : output.values()) {
- info.id = Url.encode(info.name);
- info.name = null;
- }
- if (stdout == null) {
- return output;
- } else 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);
- }
- return null;
- } finally {
- if (stdout != null) {
- stdout.flush();
- }
- }
- }
-
- private boolean isGroupVisible() {
- try {
- return groupUuid == null || groupControlFactory.controlFor(groupUuid).isVisible();
- } catch (NoSuchGroupException ex) {
- return false;
- }
- }
-
- private void printProjectBranches(PrintWriter stdout, ProjectInfo info) {
- 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(' ');
- }
- }
-
- private void addProjectBranchesInfo(ProjectInfo info, List<Ref> refs) {
- 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 = new LinkedHashMap<>();
- }
- info.branches.put(showBranch.get(i), ref.getObjectId().name());
- }
- }
- }
-
- private List<Ref> retrieveBranchRefs(ProjectState e, Repository git) {
- if (!e.statePermitsRead()) {
- return ImmutableList.of();
- }
-
- return getBranchRefs(e.getNameKey(), git);
- }
-
- private void addParentProjectInfo(
- Map<String, String> hiddenNames,
- Map<Project.NameKey, Boolean> accessibleParents,
- PermissionBackend.WithUser perm,
- ProjectState e,
- ProjectInfo info)
- throws PermissionBackendException {
- ProjectState parent = Iterables.getFirst(e.parents(), null);
- if (parent != null) {
- if (isParentAccessible(accessibleParents, perm, parent)) {
- info.parent = parent.getName();
- } else {
- info.parent = hiddenNames.get(parent.getName());
- if (info.parent == null) {
- info.parent = "?-" + (hiddenNames.size() + 1);
- hiddenNames.put(parent.getName(), info.parent);
- }
- }
- }
- }
-
- private Stream<ProjectState> filter(PermissionBackend.WithUser perm) throws BadRequestException {
- return StreamSupport.stream(scan().spliterator(), false)
- .map(projectCache::get)
- .filter(Optional::isPresent)
- .map(Optional::get)
- .filter(p -> permissionCheck(p, perm));
- }
-
- private boolean permissionCheck(ProjectState state, PermissionBackend.WithUser perm) {
- // Hidden projects(permitsRead = false) should only be accessible by the project owners.
- // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
- // be allowed for other users). Allowing project owners to access here will help them to view
- // and update the config of hidden projects easily.
- return perm.project(state.getNameKey())
- .testOrFalse(
- state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG);
- }
-
- private boolean isParentAccessible(
- Map<Project.NameKey, Boolean> checked, PermissionBackend.WithUser perm, ProjectState state)
- throws PermissionBackendException {
- Project.NameKey name = state.getNameKey();
- Boolean b = checked.get(name);
- if (b == null) {
- try {
- // Hidden projects(permitsRead = false) should only be accessible by the project owners.
- // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
- // be allowed for other users). Allowing project owners to access here will help them to
- // view
- // and update the config of hidden projects easily.
- ProjectPermission permissionToCheck =
- state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
- perm.project(name).check(permissionToCheck);
- b = true;
- } catch (AuthException denied) {
- b = false;
- }
- checked.put(name, b);
- }
- return b;
- }
-
- private Stream<Project.NameKey> scan() throws BadRequestException {
- if (matchPrefix != null) {
- checkMatchOptions(matchSubstring == null && matchRegex == null);
- return projectCache.byName(matchPrefix).stream();
- } else if (matchSubstring != null) {
- checkMatchOptions(matchPrefix == null && matchRegex == null);
- return projectCache.all().stream()
- .filter(
- p -> p.get().toLowerCase(Locale.US).contains(matchSubstring.toLowerCase(Locale.US)));
- } else if (matchRegex != null) {
- checkMatchOptions(matchPrefix == null && matchSubstring == null);
- RegexListSearcher<Project.NameKey> searcher;
- try {
- searcher = new RegexListSearcher<>(matchRegex, Project.NameKey::get);
- } catch (IllegalArgumentException e) {
- throw new BadRequestException(e.getMessage());
- }
- return searcher.search(projectCache.all().asList());
- } else {
- return projectCache.all().stream();
- }
- }
-
- private static void checkMatchOptions(boolean cond) throws BadRequestException {
- if (!cond) {
- throw new BadRequestException("specify exactly one of p/m/r");
- }
- }
-
- private void printProjectTree(
- final PrintWriter stdout, TreeMap<Project.NameKey, ProjectNode> treeMap) {
- final NavigableSet<ProjectNode> sortedNodes = new TreeSet<>();
-
- // Builds the inheritance tree using a list.
- //
- for (ProjectNode key : treeMap.values()) {
- if (key.isAllProjects()) {
- sortedNodes.add(key);
- continue;
- }
-
- ProjectNode node = treeMap.get(key.getParentName());
- if (node != null) {
- node.addChild(key);
- } else {
- sortedNodes.add(key);
- }
- }
-
- final TreeFormatter treeFormatter = new TreeFormatter(stdout);
- treeFormatter.printTree(sortedNodes);
- stdout.flush();
- }
-
- private List<Ref> getBranchRefs(Project.NameKey projectName, Repository git) {
- Ref[] result = new Ref[showBranch.size()];
- try {
- PermissionBackend.ForProject perm = permissionBackend.user(currentUser).project(projectName);
- for (int i = 0; i < showBranch.size(); i++) {
- Ref ref = git.findRef(showBranch.get(i));
- if (ref != null && ref.getObjectId() != null) {
- try {
- perm.ref(ref.getLeaf().getName()).check(RefPermission.READ);
- result[i] = ref;
- } catch (AuthException e) {
- continue;
- }
- }
- }
- } catch (IOException | PermissionBackendException e) {
- // Fall through and return what is available.
- }
- return Arrays.asList(result);
- }
-
- private static boolean hasValidRef(List<Ref> refs) {
- for (Ref ref : refs) {
- if (ref != null) {
- return true;
- }
- }
- return false;
- }
+ SortedMap<String, ProjectInfo> apply() throws Exception;
}
diff --git a/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java b/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java
new file mode 100644
index 0000000..c6f927f
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/project/ListProjectsImpl.java
@@ -0,0 +1,650 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.project;
+
+import static com.google.common.base.Strings.emptyToNull;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static com.google.common.collect.Ordering.natural;
+import static com.google.gerrit.extensions.client.ProjectState.HIDDEN;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.collect.Iterables;
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.entities.AccountGroup;
+import com.google.gerrit.entities.GroupReference;
+import com.google.gerrit.entities.Project;
+import com.google.gerrit.exceptions.NoSuchGroupException;
+import com.google.gerrit.exceptions.StorageException;
+import com.google.gerrit.extensions.common.ProjectInfo;
+import com.google.gerrit.extensions.common.WebLinkInfo;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.json.OutputFormat;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.WebLinks;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.group.GroupResolver;
+import com.google.gerrit.server.ioutil.RegexListSearcher;
+import com.google.gerrit.server.ioutil.StringUtil;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.gerrit.server.permissions.ProjectPermission;
+import com.google.gerrit.server.permissions.RefPermission;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.TreeFormatter;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.Optional;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+/**
+ * List projects visible to the calling user.
+ *
+ * <p>Implement {@code GET /projects/}, without a {@code query=} parameter.
+ */
+public class ListProjectsImpl extends AbstractListProjects {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final CurrentUser currentUser;
+ private final ProjectCache projectCache;
+ private final GroupResolver groupResolver;
+ private final GroupControl.Factory groupControlFactory;
+ private final GitRepositoryManager repoManager;
+ private final PermissionBackend permissionBackend;
+ private final ProjectNode.Factory projectNodeFactory;
+ private final WebLinks webLinks;
+
+ @Override
+ public void setFormat(OutputFormat fmt) {
+ format = fmt;
+ }
+
+ @Override
+ public void addShowBranch(String branch) {
+ showBranch.add(branch);
+ }
+
+ @Override
+ public void setShowTree(boolean showTree) {
+ this.showTree = showTree;
+ }
+
+ @Override
+ public void setFilterType(FilterType type) {
+ this.type = type;
+ }
+
+ @Override
+ public void setShowDescription(boolean showDescription) {
+ this.showDescription = showDescription;
+ }
+
+ @Override
+ public void setAll(boolean all) {
+ this.all = all;
+ }
+
+ @Override
+ public void setState(com.google.gerrit.extensions.client.ProjectState state) {
+ this.state = state;
+ }
+
+ @Override
+ public void setLimit(int limit) {
+ this.limit = limit;
+ }
+
+ @Override
+ public void setStart(int start) {
+ this.start = start;
+ }
+
+ @Override
+ public void setMatchPrefix(String matchPrefix) {
+ this.matchPrefix = matchPrefix;
+ }
+
+ @Override
+ public void setMatchSubstring(String matchSubstring) {
+ this.matchSubstring = matchSubstring;
+ }
+
+ @Override
+ public void setMatchRegex(String matchRegex) {
+ this.matchRegex = matchRegex;
+ }
+
+ @Override
+ public void setGroupUuid(AccountGroup.UUID groupUuid) {
+ this.groupUuid = groupUuid;
+ }
+
+ @Deprecated private OutputFormat format = OutputFormat.TEXT;
+ private final List<String> showBranch = new ArrayList<>();
+ private boolean showTree;
+ private FilterType type = FilterType.ALL;
+ private boolean showDescription;
+ private boolean all;
+ private com.google.gerrit.extensions.client.ProjectState state;
+ private int limit;
+ private int start;
+ private String matchPrefix;
+ private String matchSubstring;
+ private String matchRegex;
+ private AccountGroup.UUID groupUuid;
+ private final Provider<QueryProjects> queryProjectsProvider;
+ private final boolean listProjectsFromIndex;
+
+ @Inject
+ protected ListProjectsImpl(
+ CurrentUser currentUser,
+ ProjectCache projectCache,
+ GroupResolver groupResolver,
+ GroupControl.Factory groupControlFactory,
+ GitRepositoryManager repoManager,
+ PermissionBackend permissionBackend,
+ ProjectNode.Factory projectNodeFactory,
+ WebLinks webLinks,
+ Provider<QueryProjects> queryProjectsProvider,
+ @GerritServerConfig Config config) {
+ this.currentUser = currentUser;
+ this.projectCache = projectCache;
+ this.groupResolver = groupResolver;
+ this.groupControlFactory = groupControlFactory;
+ this.repoManager = repoManager;
+ this.permissionBackend = permissionBackend;
+ this.projectNodeFactory = projectNodeFactory;
+ this.webLinks = webLinks;
+ this.queryProjectsProvider = queryProjectsProvider;
+ this.listProjectsFromIndex = config.getBoolean("gerrit", "listProjectsFromIndex", false);
+ }
+
+ public List<String> getShowBranch() {
+ return showBranch;
+ }
+
+ public boolean isShowTree() {
+ return showTree;
+ }
+
+ public boolean isShowDescription() {
+ return showDescription;
+ }
+
+ public OutputFormat getFormat() {
+ return format;
+ }
+
+ @Override
+ public Response<Object> apply(TopLevelResource resource)
+ throws BadRequestException, PermissionBackendException {
+ if (format == OutputFormat.TEXT) {
+ ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ displayToStream(buf);
+ return Response.ok(
+ BinaryResult.create(buf.toByteArray())
+ .setContentType("text/plain")
+ .setCharacterEncoding(UTF_8));
+ }
+ return Response.ok(apply());
+ }
+
+ @Override
+ public SortedMap<String, ProjectInfo> apply()
+ throws BadRequestException, PermissionBackendException {
+ Optional<String> projectQuery = expressAsProjectsQuery();
+ if (projectQuery.isPresent()) {
+ return applyAsQuery(projectQuery.get());
+ }
+
+ format = OutputFormat.JSON;
+ return display(null);
+ }
+
+ private Optional<String> expressAsProjectsQuery() {
+ return listProjectsFromIndex
+ && !all
+ && state != HIDDEN
+ && isNullOrEmpty(matchPrefix)
+ && isNullOrEmpty(matchRegex)
+ && isNullOrEmpty(
+ matchSubstring) // TODO: see https://issues.gerritcodereview.com/issues/40010295
+ && type == FilterType.ALL
+ && showBranch.isEmpty()
+ && !showTree
+ ? Optional.of(stateToQuery())
+ : Optional.empty();
+ }
+
+ private String stateToQuery() {
+ return state != null
+ ? String.format("(state:%s)", state.name())
+ : "(state:active OR state:read-only)";
+ }
+
+ private SortedMap<String, ProjectInfo> applyAsQuery(String query) throws BadRequestException {
+ try {
+ return queryProjectsProvider.get().withQuery(query).withStart(start).withLimit(limit).apply()
+ .stream()
+ .collect(
+ ImmutableSortedMap.toImmutableSortedMap(
+ natural(), p -> p.name, p -> showDescription ? p : nullifyDescription(p)));
+ } catch (StorageException | MethodNotAllowedException e) {
+ logger.atWarning().withCause(e).log(
+ "Internal error while processing the query '%s' request", query);
+ throw new BadRequestException("Internal error while processing the query request");
+ }
+ }
+
+ private ProjectInfo nullifyDescription(ProjectInfo p) {
+ p.description = null;
+ return p;
+ }
+
+ private void printQueryResults(String query, PrintWriter out) throws BadRequestException {
+ try {
+ if (format.isJson()) {
+ format.newGson().toJson(applyAsQuery(query), out);
+ } else {
+ newProjectsNamesStream(query).forEach(out::println);
+ }
+ out.flush();
+ } catch (StorageException | MethodNotAllowedException e) {
+ logger.atWarning().withCause(e).log(
+ "Internal error while processing the query '%s' request", query);
+ throw new BadRequestException("Internal error while processing the query request");
+ }
+ }
+
+ private Stream<String> newProjectsNamesStream(String query)
+ throws MethodNotAllowedException, BadRequestException {
+ Stream<String> projects =
+ queryProjectsProvider.get().withQuery(query).apply().stream().map(p -> p.name).skip(start);
+ if (limit > 0) {
+ projects = projects.limit(limit);
+ }
+
+ return projects;
+ }
+
+ public void displayToStream(OutputStream displayOutputStream)
+ throws BadRequestException, PermissionBackendException {
+ PrintWriter stdout =
+ new PrintWriter(new BufferedWriter(new OutputStreamWriter(displayOutputStream, UTF_8)));
+ Optional<String> projectsQuery = expressAsProjectsQuery();
+
+ if (projectsQuery.isPresent()) {
+ printQueryResults(projectsQuery.get(), stdout);
+ } else {
+ display(stdout);
+ }
+ }
+
+ @Nullable
+ public SortedMap<String, ProjectInfo> display(@Nullable PrintWriter stdout)
+ throws BadRequestException, PermissionBackendException {
+ if (all && state != null) {
+ throw new BadRequestException("'all' and 'state' may not be used together");
+ }
+ if (!isGroupVisible()) {
+ return Collections.emptySortedMap();
+ }
+
+ int foundIndex = 0;
+ int found = 0;
+ TreeMap<String, ProjectInfo> output = new TreeMap<>();
+ Map<String, String> hiddenNames = new HashMap<>();
+ Map<Project.NameKey, Boolean> accessibleParents = new HashMap<>();
+ PermissionBackend.WithUser perm = permissionBackend.user(currentUser);
+ final TreeMap<Project.NameKey, ProjectNode> treeMap = new TreeMap<>();
+ try {
+ Iterator<ProjectState> projectStatesIt = filter(perm).iterator();
+ while (projectStatesIt.hasNext()) {
+ ProjectState e = projectStatesIt.next();
+ Project.NameKey projectName = e.getNameKey();
+ if (e.getProject().getState() == HIDDEN && !all && state != HIDDEN) {
+ // If we can't get it from the cache, pretend it's not present.
+ // If all wasn't selected, and it's HIDDEN, pretend it's not present.
+ // If state HIDDEN wasn't selected, and it's HIDDEN, pretend it's not present.
+ continue;
+ }
+
+ if (state != null && e.getProject().getState() != state) {
+ continue;
+ }
+
+ if (groupUuid != null
+ && !e.getLocalGroups()
+ .contains(GroupReference.forGroup(groupResolver.parseId(groupUuid.get())))) {
+ continue;
+ }
+
+ if (showTree && !format.isJson()) {
+ treeMap.put(projectName, projectNodeFactory.create(e.getProject(), true));
+ continue;
+ }
+
+ if (foundIndex++ < start) {
+ continue;
+ }
+ if (limit > 0 && ++found > limit) {
+ break;
+ }
+
+ ProjectInfo info = new ProjectInfo();
+ info.name = projectName.get();
+ if (showTree && format.isJson()) {
+ addParentProjectInfo(hiddenNames, accessibleParents, perm, e, info);
+ }
+
+ if (showDescription) {
+ info.description = emptyToNull(e.getProject().getDescription());
+ }
+ info.state = e.getProject().getState();
+
+ try {
+ if (!showBranch.isEmpty()) {
+ try (Repository git = repoManager.openRepository(projectName)) {
+ if (!type.matches(git)) {
+ continue;
+ }
+
+ List<Ref> refs = retrieveBranchRefs(e, git);
+ if (!hasValidRef(refs)) {
+ continue;
+ }
+
+ addProjectBranchesInfo(info, refs);
+ }
+ } else if (!showTree && type.useMatch()) {
+ try (Repository git = repoManager.openRepository(projectName)) {
+ if (!type.matches(git)) {
+ continue;
+ }
+ }
+ }
+ } catch (RepositoryNotFoundException err) {
+ // If the Git repository is gone, the project doesn't actually exist anymore.
+ continue;
+ } catch (IOException err) {
+ logger.atWarning().withCause(err).log("Unexpected error reading %s", projectName);
+ continue;
+ }
+
+ ImmutableList<WebLinkInfo> links = webLinks.getProjectLinks(projectName.get());
+ info.webLinks = links.isEmpty() ? null : links;
+
+ if (stdout == null || format.isJson()) {
+ output.put(info.name, info);
+ continue;
+ }
+
+ if (!showBranch.isEmpty()) {
+ printProjectBranches(stdout, info);
+ }
+ stdout.print(info.name);
+
+ if (info.description != null) {
+ // We still want to list every project as one-liners, hence escaping \n.
+ stdout.print(" - " + StringUtil.escapeString(info.description));
+ }
+ stdout.print('\n');
+ }
+
+ for (ProjectInfo info : output.values()) {
+ info.id = Url.encode(info.name);
+ info.name = null;
+ }
+ if (stdout == null) {
+ return output;
+ } else 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);
+ }
+ return null;
+ } finally {
+ if (stdout != null) {
+ stdout.flush();
+ }
+ }
+ }
+
+ private boolean isGroupVisible() {
+ try {
+ return groupUuid == null || groupControlFactory.controlFor(groupUuid).isVisible();
+ } catch (NoSuchGroupException ex) {
+ return false;
+ }
+ }
+
+ private void printProjectBranches(PrintWriter stdout, ProjectInfo info) {
+ 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(' ');
+ }
+ }
+
+ private void addProjectBranchesInfo(ProjectInfo info, List<Ref> refs) {
+ 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 = new LinkedHashMap<>();
+ }
+ info.branches.put(showBranch.get(i), ref.getObjectId().name());
+ }
+ }
+ }
+
+ private List<Ref> retrieveBranchRefs(ProjectState e, Repository git) {
+ if (!e.statePermitsRead()) {
+ return ImmutableList.of();
+ }
+
+ return getBranchRefs(e.getNameKey(), git);
+ }
+
+ private void addParentProjectInfo(
+ Map<String, String> hiddenNames,
+ Map<Project.NameKey, Boolean> accessibleParents,
+ PermissionBackend.WithUser perm,
+ ProjectState e,
+ ProjectInfo info)
+ throws PermissionBackendException {
+ ProjectState parent = Iterables.getFirst(e.parents(), null);
+ if (parent != null) {
+ if (isParentAccessible(accessibleParents, perm, parent)) {
+ info.parent = parent.getName();
+ } else {
+ info.parent = hiddenNames.get(parent.getName());
+ if (info.parent == null) {
+ info.parent = "?-" + (hiddenNames.size() + 1);
+ hiddenNames.put(parent.getName(), info.parent);
+ }
+ }
+ }
+ }
+
+ private Stream<ProjectState> filter(PermissionBackend.WithUser perm) throws BadRequestException {
+ return StreamSupport.stream(scan().spliterator(), false)
+ .map(projectCache::get)
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .filter(p -> permissionCheck(p, perm));
+ }
+
+ private boolean permissionCheck(ProjectState state, PermissionBackend.WithUser perm) {
+ // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+ // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
+ // be allowed for other users). Allowing project owners to access here will help them to view
+ // and update the config of hidden projects easily.
+ return perm.project(state.getNameKey())
+ .testOrFalse(
+ state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG);
+ }
+
+ private boolean isParentAccessible(
+ Map<Project.NameKey, Boolean> checked, PermissionBackend.WithUser perm, ProjectState state)
+ throws PermissionBackendException {
+ Project.NameKey name = state.getNameKey();
+ Boolean b = checked.get(name);
+ if (b == null) {
+ try {
+ // Hidden projects(permitsRead = false) should only be accessible by the project owners.
+ // READ_CONFIG is checked here because it's only allowed to project owners(ACCESS may also
+ // be allowed for other users). Allowing project owners to access here will help them to
+ // view
+ // and update the config of hidden projects easily.
+ ProjectPermission permissionToCheck =
+ state.statePermitsRead() ? ProjectPermission.ACCESS : ProjectPermission.READ_CONFIG;
+ perm.project(name).check(permissionToCheck);
+ b = true;
+ } catch (AuthException denied) {
+ b = false;
+ }
+ checked.put(name, b);
+ }
+ return b;
+ }
+
+ private Stream<Project.NameKey> scan() throws BadRequestException {
+ if (matchPrefix != null) {
+ checkMatchOptions(matchSubstring == null && matchRegex == null);
+ return projectCache.byName(matchPrefix).stream();
+ } else if (matchSubstring != null) {
+ checkMatchOptions(matchPrefix == null && matchRegex == null);
+ return projectCache.all().stream()
+ .filter(
+ p -> p.get().toLowerCase(Locale.US).contains(matchSubstring.toLowerCase(Locale.US)));
+ } else if (matchRegex != null) {
+ checkMatchOptions(matchPrefix == null && matchSubstring == null);
+ RegexListSearcher<Project.NameKey> searcher;
+ try {
+ searcher = new RegexListSearcher<>(matchRegex, Project.NameKey::get);
+ } catch (IllegalArgumentException e) {
+ throw new BadRequestException(e.getMessage());
+ }
+ return searcher.search(projectCache.all().asList());
+ } else {
+ return projectCache.all().stream();
+ }
+ }
+
+ private static void checkMatchOptions(boolean cond) throws BadRequestException {
+ if (!cond) {
+ throw new BadRequestException("specify exactly one of p/m/r");
+ }
+ }
+
+ private void printProjectTree(
+ final PrintWriter stdout, TreeMap<Project.NameKey, ProjectNode> treeMap) {
+ final NavigableSet<ProjectNode> sortedNodes = new TreeSet<>();
+
+ // Builds the inheritance tree using a list.
+ //
+ for (ProjectNode key : treeMap.values()) {
+ if (key.isAllProjects()) {
+ sortedNodes.add(key);
+ continue;
+ }
+
+ ProjectNode node = treeMap.get(key.getParentName());
+ if (node != null) {
+ node.addChild(key);
+ } else {
+ sortedNodes.add(key);
+ }
+ }
+
+ final TreeFormatter treeFormatter = new TreeFormatter(stdout);
+ treeFormatter.printTree(sortedNodes);
+ stdout.flush();
+ }
+
+ private List<Ref> getBranchRefs(Project.NameKey projectName, Repository git) {
+ Ref[] result = new Ref[showBranch.size()];
+ try {
+ PermissionBackend.ForProject perm = permissionBackend.user(currentUser).project(projectName);
+ for (int i = 0; i < showBranch.size(); i++) {
+ Ref ref = git.findRef(showBranch.get(i));
+ if (ref != null && ref.getObjectId() != null) {
+ try {
+ perm.ref(ref.getLeaf().getName()).check(RefPermission.READ);
+ result[i] = ref;
+ } catch (AuthException e) {
+ continue;
+ }
+ }
+ }
+ } catch (IOException | PermissionBackendException e) {
+ // Fall through and return what is available.
+ }
+ return Arrays.asList(result);
+ }
+
+ private static boolean hasValidRef(List<Ref> refs) {
+ for (Ref ref : refs) {
+ if (ref != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectNode.java b/java/com/google/gerrit/server/restapi/project/ProjectNode.java
index 816c69d..ff3f588 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectNode.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectNode.java
@@ -22,7 +22,7 @@
import java.util.NavigableSet;
import java.util.TreeSet;
-/** Node of a Project in a tree formatted by {@link ListProjects}. */
+/** Node of a Project in a tree formatted by {@link ListProjectsImpl}. */
public class ProjectNode implements TreeNode, Comparable<ProjectNode> {
public interface Factory {
ProjectNode create(Project project, boolean isVisible);
diff --git a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
index 5a38766..3883c8c6 100644
--- a/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
+++ b/java/com/google/gerrit/server/restapi/project/ProjectRestApiModule.java
@@ -33,6 +33,7 @@
@Override
protected void configure() {
bind(ProjectsCollection.class);
+ bind(ListProjects.class).to(ListProjectsImpl.class);
bind(DashboardsCollection.class);
DynamicMap.mapOf(binder(), BRANCH_KIND);
diff --git a/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index e711d57..7660eeb 100644
--- a/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -16,7 +16,7 @@
import static com.google.gerrit.sshd.CommandMetaData.Mode.MASTER_OR_SLAVE;
-import com.google.gerrit.server.restapi.project.ListProjects;
+import com.google.gerrit.server.restapi.project.ListProjectsImpl;
import com.google.gerrit.sshd.CommandMetaData;
import com.google.gerrit.sshd.SshCommand;
import com.google.gerrit.util.cli.Options;
@@ -28,7 +28,7 @@
description = "List projects visible to the caller",
runsAt = MASTER_OR_SLAVE)
public class ListProjectsCommand extends SshCommand {
- @Inject @Options public ListProjects impl;
+ @Inject @Options public ListProjectsImpl impl;
@Override
public void run() throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/rest/DynamicOptionsBeanParseListenerIT.java b/javatests/com/google/gerrit/acceptance/rest/DynamicOptionsBeanParseListenerIT.java
index 39f1e8d..f199b55 100644
--- a/javatests/com/google/gerrit/acceptance/rest/DynamicOptionsBeanParseListenerIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/DynamicOptionsBeanParseListenerIT.java
@@ -21,7 +21,7 @@
import com.google.gerrit.extensions.annotations.Exports;
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.server.DynamicOptions;
-import com.google.gerrit.server.restapi.project.ListProjects;
+import com.google.gerrit.server.restapi.project.ListProjectsImpl;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.inject.AbstractModule;
@@ -48,7 +48,7 @@
protected static class ListProjectsBeanListener implements DynamicOptions.BeanParseListener {
@Override
public void onBeanParseStart(String plugin, Object bean) {
- ListProjects listProjects = (ListProjects) bean;
+ ListProjectsImpl listProjects = (ListProjectsImpl) bean;
listProjects.setLimit(1);
}
@@ -60,7 +60,7 @@
@Override
public void configure() {
bind(DynamicOptions.DynamicBean.class)
- .annotatedWith(Exports.named(ListProjects.class))
+ .annotatedWith(Exports.named(ListProjectsImpl.class))
.to(ListProjectsBeanListener.class);
}
}
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ListProjectOptionsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ListProjectOptionsRestApiBindingsIT.java
new file mode 100644
index 0000000..1d209e1
--- /dev/null
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ListProjectOptionsRestApiBindingsIT.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2023 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.acceptance.rest.binding;
+
+import static com.google.gerrit.acceptance.rest.util.RestCall.Method.GET;
+import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
+import static org.apache.http.HttpStatus.SC_OK;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.rest.util.RestApiCallHelper;
+import com.google.gerrit.acceptance.rest.util.RestCall;
+import com.google.gerrit.server.group.SystemGroupBackend;
+import org.junit.Test;
+
+public class ListProjectOptionsRestApiBindingsIT extends AbstractDaemonTest {
+ private static final ImmutableList<RestCall> LIST_PROJECTS_WITH_OPTIONS =
+ ImmutableList.of(
+ // =========================
+ // === Supported options ===
+ // =========================
+ get200OK("/projects/?show-branch=refs/heads/master"),
+ get200OK("/projects/?b=refs/heads/master"),
+ get200OK("/projects/?format=TEXT"),
+ get200OK("/projects/?format=JSON"),
+ get200OK("/projects/?format=JSON_COMPACT"),
+ get200OK("/projects/?tree"),
+ get200OK("/projects/?tree=true"),
+ get200OK("/projects/?tree=false"),
+ get200OK("/projects/?t"),
+ get200OK("/projects/?t=true"),
+ get200OK("/projects/?t=false"),
+ get200OK("/projects/?type=ALL"),
+ get200OK("/projects/?type=CODE"),
+ get200OK("/projects/?type=PERMISSIONS"),
+ get200OK("/projects/?description"),
+ get200OK("/projects/?description=true"),
+ get200OK("/projects/?description=false"),
+ get200OK("/projects/?d"),
+ get200OK("/projects/?d=true"),
+ get200OK("/projects/?d=false"),
+ get200OK("/projects/?all"),
+ get200OK("/projects/?all=true"),
+ get200OK("/projects/?all=false"),
+ get200OK("/projects/?state=ACTIVE"),
+ get200OK("/projects/?state=READ_ONLY"),
+ get200OK("/projects/?state=HIDDEN"),
+ get200OK("/projects/?limit=10"),
+ get200OK("/projects/?n=10"),
+ get200OK("/projects/?start=10"),
+ get200OK("/projects/?S=10"),
+ get200OK("/projects/?prefix=my-prefix"),
+ get200OK("/projects/?p=my-prefix"),
+ get200OK("/projects/?match=my-match"),
+ get200OK("/projects/?m=my-match"),
+ get200OK("/projects/?r=my-regex"),
+ get200OK("/projects/?has-acl-for=" + SystemGroupBackend.ANONYMOUS_USERS.get()),
+
+ // ===========================
+ // === Unsupported options ===
+ // ===========================
+ get400BadRequest("/projects/?unknown", "\"--unknown\" is not a valid option"),
+ get400BadRequest("/projects/?unknown", "\"--unknown\" is not a valid option"),
+ get400BadRequest(
+ "/projects/?format=UNKNOWN", "\"UNKNOWN\" is not a valid value for \"--format\""),
+ get400BadRequest("/projects/?tree=UNKNOWN", "invalid boolean \"tree=UNKNOWN\""),
+ get400BadRequest("/projects/?t=UNKNOWN", "invalid boolean \"t=UNKNOWN\""),
+ get400BadRequest(
+ "/projects/?type=UNKNOWN", "\"UNKNOWN\" is not a valid value for \"--type\""),
+ get400BadRequest(
+ "/projects/?description=UNKNOWN", "invalid boolean \"description=UNKNOWN\""),
+ get400BadRequest("/projects/?d=UNKNOWN", "invalid boolean \"d=UNKNOWN\""),
+ get400BadRequest("/projects/?all=UNKNOWN", "invalid boolean \"all=UNKNOWN\""),
+ get400BadRequest(
+ "/projects/?state=UNKNOWN", "\"UNKNOWN\" is not a valid value for \"--state\""),
+ get400BadRequest("/projects/?n=UNKNOWN", "\"UNKNOWN\" is not a valid value for \"-n\""),
+ get400BadRequest(
+ "/projects/?start=UNKNOWN", "\"UNKNOWN\" is not a valid value for \"--start\""),
+ get400BadRequest("/projects/?S=UNKNOWN", "\"UNKNOWN\" is not a valid value for \"-S\""),
+ get400BadRequest("/projects/?has-acl-for=UNKNOWN", "Group \"UNKNOWN\" does not exist"));
+
+ private static RestCall get200OK(String uriFormat) {
+ return RestCall.builder(GET, uriFormat).expectedResponseCode(SC_OK).build();
+ }
+
+ private static RestCall get400BadRequest(String uriFormat, String expectedMessage) {
+ return RestCall.builder(GET, uriFormat)
+ .expectedResponseCode(SC_BAD_REQUEST)
+ .expectedMessage(expectedMessage)
+ .build();
+ }
+
+ @Test
+ public void listProjectsWithOptions() throws Exception {
+ RestApiCallHelper.execute(adminRestSession, LIST_PROJECTS_WITH_OPTIONS);
+ }
+}
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
index d4e463d..9370cfe 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/ListProjectsIT.java
@@ -43,7 +43,7 @@
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.server.project.ProjectCacheImpl;
-import com.google.gerrit.server.restapi.project.ListProjects;
+import com.google.gerrit.server.restapi.project.ListProjectsImpl;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.inject.Inject;
@@ -59,7 +59,7 @@
public class ListProjectsIT extends AbstractDaemonTest {
@Inject private ProjectOperations projectOperations;
@Inject private RequestScopeOperations requestScopeOperations;
- @Inject private ListProjects listProjects;
+ @Inject private ListProjectsImpl listProjects;
@Test
public void listProjects() throws Exception {
diff --git a/plugins/gitiles b/plugins/gitiles
index 20f65c2..4e8bd70 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit 20f65c2067b9190d1c85fbf61e5d72edf4493724
+Subproject commit 4e8bd706e87eb11e3cfe2bfa9bbcb29020f39482